小编典典

CSS 应该总是在 Javascript 之前吗?

all

在订购 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给我一个 500ms 延迟传输 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 时,页面需要 1.5 秒才能呈现:

CSS优先

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

JavaScript优先

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

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

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

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


阅读 97

收藏
2022-02-28

共1个答案

小编典典

这是一个非常有趣的问题。我总是把我的 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 中的s 之前<head>

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

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

为什么?

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

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

然而,现代浏览器(包括我上面测试过的所有浏览器)已经实现了
推测解析
,浏览器在 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
中奇妙的新远程调试器,并且网络选项卡显示浏览器实际上正在等待下载CSS
直到 JavaScript 完全加载——换句话说, 即使是最新版本的 Android 版 WebKit 似乎也不支持推测解析。
我怀疑它可能由于移动设备固有的 CPU、内存和/或网络限制而被关闭。

代码

原谅马虎——这是问答环节。

应用程序.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

2022-02-28