CSS 应该总是在 Javascript 之前吗?

IT技术 javascript css performance
2021-01-30 04:02:39

在网上无数地方,我看到了在 JavaScript 之前包含 CSS 的建议。推理通常是这种形式

在订购 CSS 和 JavaScript 时,您希望 CSS 排在第一位。原因是渲染线程拥有渲染页面所需的所有样式信息。如果 JavaScript 包含首先出现,则 JavaScript 引擎必须在继续处理下一组资源之前解析所有内容。这意味着渲染线程不能完全显示页面,因为它没有它需要的所有样式。

我的实际测试揭示了一些完全不同的东西:

我的测试线束

我使用以下 Ruby 脚本为各种资源生成特定延迟:

require 'rubygems'
require 'eventmachine'
require 'evma_httpserver'
require 'date'

class Handler  < EventMachine::Connection
  include EventMachine::HttpServer

  def process_http_request
    resp = EventMachine::DelegatedHttpResponse.new( self )

    return unless @http_query_string

    path = @http_path_info
    array = @http_query_string.split("&").map{|s| s.split("=")}.flatten
    parsed = Hash[*array]

    delay = parsed["delay"].to_i / 1000.0
    jsdelay = parsed["jsdelay"].to_i

    delay = 5 if (delay > 5)
    jsdelay = 5000 if (jsdelay > 5000)

    delay = 0 if (delay < 0) 
    jsdelay = 0 if (jsdelay < 0)

    # Block which fulfills the request
    operation = proc do
      sleep delay 

      if path.match(/.js$/)
        resp.status = 200
        resp.headers["Content-Type"] = "text/javascript"
        resp.content = "(function(){
            var start = new Date();
            while(new Date() - start < #{jsdelay}){}
          })();"
      end
      if path.match(/.css$/)
        resp.status = 200
        resp.headers["Content-Type"] = "text/css"
        resp.content = "body {font-size: 50px;}"
      end
    end

    # Callback block to execute once the request is fulfilled
    callback = proc do |res|
        resp.send_response
    end

    # Let the thread pool (20 Ruby threads) handle request
    EM.defer(operation, callback)
  end
end

EventMachine::run {
  EventMachine::start_server("0.0.0.0", 8081, Handler)
  puts "Listening..."
}

上面的迷你服务器允许我为 JavaScript 文件(服务器和客户端)和任意 CSS 延迟设置任意延迟。例如,http://10.0.0.50:8081/test.css?delay=500给我一个 500 毫秒的延迟传输 CSS。

我使用以下页面进行测试。

<!DOCTYPE html>
<html>
  <head>
      <title>test</title>
      <script type='text/javascript'>
          var startTime = new Date();
      </script>
      <link href="http://10.0.0.50:8081/test.css?delay=500" type="text/css" rel="stylesheet">
      <script type="text/javascript" src="http://10.0.0.50:8081/test2.js?delay=400&amp;jsdelay=1000"></script> 
  </head>
  <body>
    <p>
      Elapsed time is: 
      <script type='text/javascript'>
        document.write(new Date() - startTime);
      </script>
    </p>    
  </body>
</html>

当我首先包含 CSS 时,页面需要 1.5 秒来呈现:

CSS优先

当我首先包含 JavaScript 时,页面需要 1.4 秒来呈现:

JavaScript优先

我在 Chrome、Firefox 和 Internet Explorer 中得到了类似的结果。然而,在 Opera 中,顺序并不重要。

似乎正在发生的事情是 JavaScript 解释器拒绝启动,直到所有 CSS 下载完毕。因此,随着 JavaScript 线程获得更多运行时间,首先包含 JavaScript 似乎更有效。

我是否遗漏了什么,建议在 JavaScript 包含之前放置 CSS 包含不正确?

很明显,我们可以添加异步或使用 setTimeout 来释放渲染线程或将 JavaScript 代码放在页脚中,或者使用 JavaScript 加载器。这里的重点是关于头部中基本 JavaScript 位和 CSS 位的排序。

6个回答

这是一个非常有趣的问题。我总是把我的 CSS 放在我<link href="...">的 JS 之前,<script src="...">因为“我读过一次它更好。” 所以,你是对的;现在是我们进行一些实际研究的时候了!

我在 Node 中设置了自己的测试工具(代码如下)。基本上,我:

  • 确保没有 HTTP 缓存,因此每次加载页面时浏览器都必须进行完整下载。
  • 为了模拟现实,我包含了 jQuery 和H5BP CSS(所以有大量的脚本/CSS 需要解析)
  • 设置两个页面 - 一个在脚本之前使用 CSS,一个在脚本之后使用 CSS。
  • 记录了外部脚本<head>执行的时间
  • 记录了里面的内联脚本<body>执行需要多长时间,类似于DOMReady.
  • 将 CSS 和/或脚本延迟 500 毫秒发送到浏览器。
  • 在 3 个主要浏览器中运行测试 20 次。

结果

首先,将 CSS 文件延迟 500 毫秒:

     Browser: Chrome 18    | IE 9         | Firefox 9
         CSS: first  last  | first  last  | first last
=======================================================
Header Exec |              |              |
Average     | 583ms  36ms  | 559ms  42ms  | 565ms 49ms
St Dev      | 15ms   12ms  | 9ms    7ms   | 13ms  6ms
------------|--------------|--------------|------------
Body Exec   |              |              |
Average     | 584ms  521ms | 559ms  513ms | 565ms 519ms
St Dev      | 15ms   9ms   | 9ms    5ms   | 13ms  7ms

接下来,我将 jQuery 设置为延迟 500 毫秒而不是 CSS:

     Browser: Chrome 18    | IE 9         | Firefox 9
         CSS: first  last  | first  last  | first last
=======================================================
Header Exec |              |              |
Average     | 597ms  556ms | 562ms  559ms | 564ms 564ms
St Dev      | 14ms   12ms  | 11ms   7ms   | 8ms   8ms
------------|--------------|--------------|------------
Body Exec   |              |              |
Average     | 598ms  557ms | 563ms  560ms | 564ms 565ms
St Dev      | 14ms   12ms  | 10ms   7ms   | 8ms   8ms

最后,我将jQuery 和 CSS设置为延迟 500 毫秒:

     Browser: Chrome 18    | IE 9         | Firefox 9
         CSS: first  last  | first  last  | first last
=======================================================
Header Exec |              |              |
Average     | 620ms  560ms | 577ms  577ms | 571ms 567ms
St Dev      | 16ms   11ms  | 19ms   9ms   | 9ms   10ms
------------|--------------|--------------|------------
Body Exec   |              |              |
Average     | 623ms  561ms | 578ms  580ms | 571ms 568ms
St Dev      | 18ms   11ms  | 19ms   9ms   | 9ms   10ms

结论

首先,重要的是要注意,我在假设<head>您的文档位于 的 中(而不是 的末尾<body>有脚本关于为什么您可能会<head>在文档末尾与您的脚本链接的原因有多种争论,但这超出了本答案的范围。这是严格的关于<script>s是否应该在<link>s之前<head>

在现代桌面浏览器中,看起来首先链接到 CSS永远不会提供性能提升。当 CSS 和脚本都被延迟时,将 CSS 放在脚本之后会给你带来微不足道的收益,但当 CSS 被延迟时会给你带来很大的收益。(由last第一组结果中列显示。)

鉴于最后链接到 CSS 似乎不会损害性能,但在某些情况下可以提供收益,如果旧浏览器的性能不是问题那么您应该只在桌面浏览器上链接到外部脚本链接到外部样式表继续阅读移动情况。

为什么?

历史上,当浏览器遇到<script>指向外部资源标签时,浏览器会停止解析 HTML,检索脚本,执行它,然后继续解析 HTML。相反,如果浏览器遇到<link>外部样式表的 ,它会在获取 CSS 文件时继续解析 HTML(并行)。

因此,广泛重复的建议是将样式表放在首位——它们会先下载,第一个下载的脚本可以并行加载。

然而,现代浏览器(包括我在上面测试过的所有浏览器)已经实现了推测解析,浏览器在 HTML 中“向前看”并脚本下载和执行之前开始下载资源

在没有推测性解析的旧浏览器中,将脚本放在首位会影响性能,因为它们不会并行下载。

浏览器支持

推测性解析首先在:(以及截至 2012 年 1 月使用此版本或更高版本的全球桌面浏览器用户的百分比)

  • Chrome 1 (WebKit 525) (100%)
  • IE 8 (75%)
  • 火狐 3.5 (96%)
  • Safari 4 (99%)
  • 歌剧 11.60 (85%)

总的来说,目前使用的桌面浏览器中大约有 85% 支持推测加载。将脚本放在 CSS 之前会对全球15% 的用户造成性能损失YMMV 基于您网站的特定受众。(请记住,这个数字正在缩小。)

在移动浏览器上,由于移动浏览器和操作系统环境的异质性,要获得明确的数字有点困难。由于推测性渲染在 WebKit 525(2008 年 3 月发布)中实现,并且几乎所有有value的移动浏览器都基于 WebKit,我们可以得出结论,“大多数”移动浏览器应该支持它。根据quirksmode,iOS 2.2/Android 1.0 使用 WebKit 525。我不知道 Windows Phone 是什么样子。

然而,我在我的 Android 4 设备上运行了测试,虽然我看到的数字与桌面结果相似,但我将它连接到Chrome for Android 中出色的新远程调试器,网络选项卡显示浏览器实际上正在等待下载CSS 直到 JavaScript 完全加载——换句话说,即使是最新版本的 Android 版 WebKit 似乎也不支持推测性解析。 我怀疑它可能由于移动设备固有的 CPU、内存和/或网络限制而被关闭。

代码

原谅我的草率——这是 Q&D。

应用程序.js

var express = require('express')
, app = express.createServer()
, fs = require('fs');

app.listen(90);

var file={};
fs.readdirSync('.').forEach(function(f) {
    console.log(f)
    file[f] = fs.readFileSync(f);
    if (f != 'jquery.js' && f != 'style.css') app.get('/' + f, function(req,res) {
        res.contentType(f);
        res.send(file[f]);
    });
});


app.get('/jquery.js', function(req,res) {
    setTimeout(function() {
        res.contentType('text/javascript');
        res.send(file['jquery.js']);
    }, 500);
});

app.get('/style.css', function(req,res) {
    setTimeout(function() {
        res.contentType('text/css');
        res.send(file['style.css']);
    }, 500);
});


var headresults={
    css: [],
    js: []
}, bodyresults={
    css: [],
    js: []
}
app.post('/result/:type/:time/:exec', function(req,res) {
    headresults[req.params.type].push(parseInt(req.params.time, 10));
    bodyresults[req.params.type].push(parseInt(req.params.exec, 10));
    res.end();
});

app.get('/result/:type', function(req,res) {
    var o = '';
    headresults[req.params.type].forEach(function(i) {
        o+='\n' + i;
    });
    o+='\n';
    bodyresults[req.params.type].forEach(function(i) {
        o+='\n' + i;
    });
    res.send(o);
});

css.html

<!DOCTYPE html>
<html>
    <head>
        <title>CSS first</title>
        <script>var start = Date.now();</script>
        <link rel="stylesheet" href="style.css">
        <script src="jquery.js"></script>
        <script src="test.js"></script>
    </head>
    <body>
        <script>document.write(jsload - start);bodyexec=Date.now()</script>
    </body>
</html>

js.html

<!DOCTYPE html>
<html>
    <head>
        <title>CSS first</title>
        <script>var start = Date.now();</script>
        <script src="jquery.js"></script>
        <script src="test.js"></script>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <script>document.write(jsload - start);bodyexec=Date.now()</script>
    </body>
</html>

测试.js

var jsload = Date.now();


$(function() {
    $.post('/result' + location.pathname.replace('.html','') + '/' + (jsload - start) + '/' + (bodyexec - start));
});

jquery.js 是jquery-1.7.1.min.js

关于@josh3736 关于移动设备逆向的更新......这是一个例子,不要对这一重大变化大发雷霆。我很好奇其他移动浏览器的行为(webkit、gecko、presto、trident 等),因为移动中的性能通常更重要。
2021-03-23 04:02:39
“首先,重要的是要注意,我是在假设您在<head>文档的末尾(而不是<body>. 我会在答案中更早地强调这一点,就像在顶部一样。从几乎任何角度来看(当然不是性能角度),包含一个引用外部文件scriptinhead几乎永远不会正确。我不记得在现实生活中曾经不得不这样做。内联脚本的奇数行或两行可能,但仅此而已。默认的,没有非常好的相反的原因,应该是身体的末端。
2021-03-29 04:02:39
您还应该尝试在打印 css/js 时增加一些速度,以模拟慢速服务器的速度。
2021-04-05 04:02:39
如果你使用延迟或异步呢?那会改变吗?(尝试使用内联脚本和不使用内联脚本)这会改变吗?
2021-04-09 04:02:39
这是一个很棒的答案,感谢您使用科学!根据您的结果“在现代浏览器中,首先链接到 CSS 似乎永远不会提供性能提升”,我认为问题标题的答案是肯定的,CSS 优先的旧建议显然无效。
2021-04-13 04:02:39

将 CSS 放在 JavaScript 之前有两个主要原因。

  1. 旧浏览器(Internet Explorer 6-7、Firefox 2 等)在开始下载脚本时会阻止所有后续下载。因此,如果您a.js遵循b.css它们将按顺序下载:首先 a 然后 b。如果您b.css跟随a.js它们它们将被并行下载,因此页面加载速度更快。

  2. 在下载所有样式表之前不会渲染任何内容 - 这在所有浏览器中都是如此。脚本是不同的 - 它们会阻止页面中脚本标记下方的所有 DOM 元素的呈现如果您将脚本放在 HEAD 中,则意味着在下载所有样式表和所有脚本之前,整个页面都将无法呈现。虽然阻止样式表的所有呈现是有意义的(这样您可以在第一时间获得正确的样式并避免无样式内容 FOUC 的闪现),但阻止脚本的整个页面的呈现是没有意义的。通常,脚本不会影响任何 DOM 元素或仅影响一部分 DOM 元素。最好在页面中尽可能低地加载脚本,或者更好地异步加载它们。

使用Cuzillion创建示例很有趣例如,此页面的 HEAD 中有一个脚本,因此整个页面在下载完成之前都是空白的。但是,如果我们将脚本移到 BODY 块的末尾,页面标题就会呈现,因为这些 DOM 元素出现在 SCRIPT 标记上方,如您在此页面上看到的

尊敬的史蒂夫击败了我的答案,但我会添加一篇与他提到的内容相关的文章:stevesouders.com/blog/2009/04/27/...
2021-03-14 04:02:39
嘿,你能告诉我为什么有人会用@import指令链接 CSS 文件吗?
2021-03-25 04:02:39
所以,我们应该把jQuery+ jQuery UI+$(document).ready(function () { });在页面的结束?它会一直按预期工作吗?
2021-03-25 04:02:39
看看哪些浏览器支持该async属性,史蒂夫在说“甚至更好地异步加载它们”时在这里推荐的——stackoverflow.com/questions/1834077/...
2021-04-09 04:02:39
2) 的来源是什么,如果是真的,您能否解释为什么有时页面会完成加载内容,然后在一两秒钟后应用 CSS?(这种情况很少发生在我自己的页面上,CSS 位于 <head> 标签中)
2021-04-12 04:02:39

我不会太强调你得到的结果,我相信这是主观的,但我有理由向你解释,最好在js之前放入CSS。

在您的网站加载过程中,您会看到两种情况:

案例 1:白屏 > 无样式网站 > 样式网站 > 交互 > 样式和交互网站

案例 2:白屏 > 无样式网站 > 交互 > 样式网站 > 样式和交互网站


老实说,我无法想象有人会选择案例 2。这意味着使用慢速互联网连接的访问​​者将面临一个无样式的网站,这允许他们使用 Javascript 与它进行交互(因为它已经加载了)。此外,花在查看无样式网站上的时间会以这种方式最大化。为什么会有人想要那个?

jQuery 声明时,它也能更好地工作

“使用依赖于 CSS 样式属性值的脚本时,在引用脚本之前引用外部样式表或嵌入样式元素非常重要”。

当文件以错误的顺序(首先是 JS,然后是 CSS)加载时,任何依赖于 CSS 文件中设置的属性(例如 div 的宽度或高度)的 Javascript 代码都将无法正确加载。似乎在错误的加载顺序下,正确的属性“有时”是 Javascript 已知的(也许这是由竞争条件引起的?)。根据所使用的浏览器,这种效果似乎更大或更小。

@Jonnio 如果您的 JS 有依赖项,那么您应该明确该依赖项。否则,您将始终遇到罕见的时间问题。ES6 module是实现这一目标的好方法,但也可以使用许多库。
2021-04-10 04:02:39
您将如何保证在 ja​​vascript 执行之前加载所有 css?你可以吗?或者您的 javascript 是否应该足够健壮以处理可能不一定加载样式的情况。
2021-04-13 04:02:39

您的测试是在您的个人计算机上还是在 Web 服务器上进行的?它是一个空白页面,还是一个包含图像、数据库等的复杂在线系统?您的脚本是执行简单的悬停事件操作,还是您的网站如何呈现并与用户交互的核心组件?这里有几件事情需要考虑,当您冒险进行高质量的 Web 开发时,这些建议的相关性几乎总是成为规则。

“将样式表放在顶部,脚本放在底部”规则的目的是,总的来说,这是实现最佳渐进式渲染的最佳方式这对用户体验至关重要。

抛开其他所有:假设您的测试是有效的,并且您确实产生了与流行规则相反的结果,这并不奇怪,真的。每个网站(以及使整个内容出现在用户屏幕上所需的一切)都是不同的,而且互联网也在不断发展。

我很欣赏你在大胆提出的观点,但OP在谈论你时有所不同的顺序发生了什么在上面,既没有在底部。
2021-03-31 04:02:39
因此,“假设 [他的] 测试是有效的。”
2021-04-13 04:02:39

出于不同的原因,我在 Javascript 之前包含了 CSS 文件。

如果我的 Javascript 需要对某些页面元素进行动态调整大小(对于那些 CSS 确实是后面主要内容的极端情况),那么在 JS 运行之后加载 CSS 可能会导致竞争条件,即在 CSS 样式之前调整元素大小被应用,因此当样式最终启动时看起来很奇怪。如果我事先加载 CSS,我可以保证事情按预期顺序运行,并且最终布局是我想要的。

@brunoais:尽管某些布局只能使用 Javascript 创建。例如,任何需要动态调整大小的东西都必须通过 Javascript 制作,而有些东西(比如大小为 100% - 20px)需要 Javascript 在旧浏览器中可移植地完成。
2021-03-14 04:02:39
jcolebrand:是的,我想我在写这篇文章的时候还没有喝足够的咖啡。(回想起来,我想重要的事情只是避免动态加载 CSS 并将 JS 放在 domReady 事件中,如果您需要进行动态调整大小)
2021-03-16 04:02:39
这将有一天在某些浏览器上中断。我不是在猜测。
2021-03-26 04:02:39
@missingno 在这些情况下,无论如何只需使用 DOMContentLoaded 事件。但我明白你的意思。(愚蠢的IE!)
2021-03-27 04:02:39
脚本不应更改任何显示。这就是 CSS 的工作。HTML = 内容,CSS = 如何显示内容,javascript 动态改变内容。此外,js 应该只在 DOMContentLoaded 被触发后(或同时)在一些很小但非常具体的情况下起作用。
2021-04-10 04:02:39