协慌网

登录 贡献 社区

建议在 JavaScript 无效之前包含 CSS 吗?

在网上无数的地方,我已经看到了在 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 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 线程会获得更多的运行时间。

我错过了什么,建议将 CSS 包含在 JavaScript 之前包括不正确吗?

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

答案

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

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

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

结果

首先,CSS 文件延迟 500ms:

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 设置为延迟 500ms 而不是 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 的由 500ms 的延迟:

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>是否应该在<head>中的<head> <link>之前。

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

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

为什么?

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

因此,广泛重复的建议首先放置样式表 - 他们将首先下载,并且可以并行加载下载的第一个脚本。

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

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

浏览器支持

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

  • Chrome 1(WebKit 525)(100%)
  • IE 8(75%)
  • Firefox 3.5(96%)
  • Safari 4(99%)
  • Opera 11.60(85%)

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

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

但是,我在我的 Android 4 设备上运行测试,虽然我看到类似于桌面结果的数字,但我将它连接到 Chrome for Android 中的神奇新远程调试器,而 Network 选项卡显示浏览器实际上正在等待下载 CSS 直到 JavaScripts 完全加载 - 换句话说, 即使是最新版本的 WebKit for Android 似乎也不支持推测性解析。我怀疑它可能因移动设备固有的 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));
});

jquery.js 是jquery-1.7.1.min.js

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

  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 标记之上, 如此页面所示

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

在加载网站期间,您会看到两种情况:

案例 1:白色屏幕 > 无框架网站 > 风格网站 > 交互 > 风格和互动网站

案例 2:白屏 > 无框网站 > 互动 > 风格网站 > 风格和互动网站


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

它也可以像jQuery那样更好地工作

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

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