开发者问题收集

CSS 是否应始终位于 JavaScript 之前?

2012-02-14
123859

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

When it comes to ordering your CSS and JavaScript, you want your CSS to come first. The reason is that the rendering thread has all the style information it needs to render the page. If the JavaScript includes come first, the JavaScript engine has to parse it all before continuing on to the next set of resources. This means the rendering thread can't completely show the page, since it doesn't have all the styles it needs.

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

我的测试工具

我使用以下 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 first

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

JavaScript first

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

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

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

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

3个回答

这是一个非常有趣的问题。我总是将 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> 是否应该在 <head> 中的 <link> 之前。

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

代码

请原谅我的草率——这是问答。

文件 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));
});

jQuery 原为 jquery-1.7.1.min.js

josh3736
2012-02-14

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

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

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

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

Steve Souders
2012-02-14

我不会过分强调您得到的结果。我认为这是主观的,但我有理由向您解释,最好先放入 CSS,然后再放入 JavaScript。

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

情况 1 :白屏 → 无样式的网站 → 样式化的网站 → 交互 → 样式化和交互的网站

情况 2 :白屏 → 无样式的网站 → 交互 → 样式化的网站 → 样式化和交互的网站

老实说,我无法想象有人会选择情况 2。这意味着使用慢速 Internet 连接的访问​​者将面临无样式的网站,这使他们能够使用 JavaScript 与其交互(因为 JavaScript 已加载)。此外,通过这种方式,可以最大限度地延长浏览无样式网站的时间。为什么有人想要那样?

它也能更好地工作,正如 jQuery 所述

"When using scripts that rely on the value of CSS style properties, it's important to reference external stylesheets or embed style elements before referencing the scripts".

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

defau1t
2012-02-14