CSS应该总是在Javascript之前吗?结果结论为什么?法典

2022-08-29 22:13:46

在网上的无数地方,我看到了在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延迟。例如,给我一个500毫秒的延迟来传输CSS。http://10.0.0.50:8081/test.css?delay=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线程获得了更多的运行时间。

我是否遗漏了一些东西,在JavaScript包含之前放置CSS包含的建议是不正确的?

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


答案 1

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

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

  • 确保没有HTTP缓存,因此浏览器每次加载页面时都必须进行完整下载。
  • 为了模拟现实,我加入了jQuery和H5BP CSS(所以有相当数量的脚本/CSS需要解析)
  • 设置两个页面 - 一个在脚本之前使用CSS,一个在脚本之后使用CSS。
  • 记录了<头>中的外部脚本执行所需的时间
  • 记录了<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设置为延迟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

结论

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

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

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

为什么?

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

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

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

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

浏览器支持

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

  • Chrome 1 (WebKit 525) (100%)
  • IE 8 (75%)
  • 火狐 3.5 (96%)
  • 野生动物园 4 (99%)
  • 歌剧 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设备上运行了测试,虽然我看到的数字与桌面结果相似,但我将其连接到Android版Chrome中出色的新远程调试器,并且“网络”选项卡显示浏览器实际上正在等待下载CSS,直到JavaScripts完全加载 - 换句话说,即使是最新版本的WebKit for Android似乎也不支持推测性解析。 我怀疑由于移动设备固有的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


答案 2

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

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

  2. 在下载所有样式表之前,不会呈现任何内容 - 在所有浏览器中都是如此。脚本是不同的 - 它们阻止呈现页面中脚本标记下的所有 DOM 元素。如果你把你的脚本放在HEAD中,那么这意味着在下载所有样式表和所有脚本之前,整个页面都被阻止呈现。虽然阻止样式表的所有呈现是有意义的(因此您第一次就获得了正确的样式并避免了无样式内容FOUC的闪烁),但阻止脚本的整个页面的呈现是没有意义的。通常,脚本不会影响任何 DOM 元素,也不会影响 DOM 元素的一部分。最好在页面中加载脚本的优先级尽可能低,或者最好以异步方式加载脚本。

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