CSS 应该总是在 JavaScript 之前吗?

新手上路,请多包涵

在网上无数地方我都看到了在 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 给我传输 CSS 的延迟 500 毫秒。

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

 <!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 似乎更有效。

我错过了什么吗?将 CSS includes 放在 JavaScript includes 之前的建议不正确吗?

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

原文由 Sam Saffron 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 422
2 个回答

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

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

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

结果

首先,CSS文件延迟500毫秒(单位为毫秒):

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

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

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

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

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

结论

首先,请务必注意,我假设您的脚本位于文档的 <head> 中(与 <body> 的末尾相对)。关于为什么您可以在 <head> 与文档末尾链接到您的脚本,存在各种争论,但这超出了本答案的范围。这完全是关于 <script> s 是否应该在 <link> 中的 <head> 之前。

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

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

为什么?

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

因此,广为流传的将样式表放在首位的建议——样式表将首先下载,第一个下载的脚本可以并行加载。

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

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

浏览器支持

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

总的来说,今天使用的大约 85% 的桌面浏览器支持推测加载。将脚本放在 CSS 之前会对 全球 15% 的用户造成性能损失;您的里程可能会根据您网站的特定受众而有所不同。 (请记住,这个数字正在缩小。)

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

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

代码

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

文件 app.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>

文件 test.js

 var jsload = Date.now();

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

jQueryjquery-1.7.1.min.js

原文由 josh3736 发布,翻译遵循 CC BY-SA 4.0 许可协议

将 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 标记之上,如您在 本页 上所见。

原文由 Steve Souders 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题