网络埋伏纪事

如何以及为什么应该内联关键 CSS

网络埋伏纪事 · 2016-11-16翻译 · 1232阅读 原文链接

在互联网早期年代,网站主要是用来显示基于文本的信息。慢慢地,连接速度提升了,用户已经能相当快地下载高分辨率的图像和视频了。现在网站就不仅仅是以文本的形式提供所需信息了。网站正变得越来越复杂,带有 CSS 和 JavaScript 框架,以及大量的插件等等,加载这些东西所需的所有文件要花一些时间。

更快的网站可以带来更佳的用户体验,这在网站成功中可以产生巨大的差异。那么要开始提升性能,开发者能做哪些事情呢?其中之一是将关键 CSS 内联,同时异步加载非关键 CSS。本文将一个一个学习这些知识点,以帮助你提升网站的性能。

什么是关键(Critical) CSS?

项目中的关键 CSS 是用来格式化网站首屏(above-the-fold)内容的 CSS。首屏内容是用户在你的网站上最先看到的内容,可以包括导航栏和其它元素。所以,尽可能快地格式化和渲染网站的这部分是很重要的。

这里我想指出的一件事情是,网站访问者可能使用五花八门的带有不同视口的设备访问你的网站。所以,首屏内容本身是搞不定的。要解决这个问题,你还应该把与基础布局和排版相关的所有 CSS 也看作是关键的。

现代 Web 开发实践通常会推荐你内联关键 CSS。像下面这样,把关键 CSS 放在网页中:

<!doctype html>
<html>
  <head>
    <title>一个优化了的网页</title>
    <style type="text/css"> (压缩了的关键 CSS 放在这里) </style>
  </head>
  <body>
     (你的HTML标记)
  </body>
</html>

为什么内联是必须的?

如果你导航到 Google PageSpeed Insights,分析你的网页之一,你可能会看到警告你要通过内联关键 CSS,并且异步加载阻塞渲染的样式,来优化你交付的 CSS。

浏览器只有在加载完成所有 CSS 文件后,才会渲染网页的首屏内容。如果有很多文件需要加载,这就是个大事。

并非所有用户有快速互联网连接,如果在他们可以看到他们冲着来访问的实际内容之前, 还要等待库、CSS 和 JavaScript 下载,这可能是很令人失望的。即使是有快速连接的用户,在某些情况下,也会丧失连接,比如通过隧道时。此时,如果你的网站有内联的关键 CSS,而且在显示主要内容之前不加载其它文件的话,用户依然可以访问主要内容。

如下的图像阐述了普通网页和优化了的网页的差别。正如你所见,优化了的版本会让用户早 0.5 秒访问到内容。在有很多额外的库需要加载时,这种提升会更明显。

Inline CSS loading stages

你应该内联关键 CSS 吗?

得视情况而定。如果没有用很重的框架或者库,并且你自己的 CSS 文件也够小,那么就不需要内联 CSS。

如果你用了像 BootStrap 这样的框架,可能你用不到该框架提供的所有功能。在这种情况下,你可以只从框架的样式表中提取关键 CSS,然后异步加载实际的框架。这样也能显著地提升网站的速度。

在内联时,样式表可以被缓存。HTML 通常是不被缓存的(这样做也不是个好主意!)。这意味着二者之间偶然会有区别。在更新时要记住这点!此外,内联的 CSS 会导致每次用户加载网站时网页会大一点。比如,如果网站中每个页面有 30 KB 的内联 CSS,那么一个用户访问 10 个页面就会消耗掉用户 300KB。这听起来好像也不是什么大事,但是在世界的某些地方数据是昂贵的,还有人用的是 3G/4G 数据套餐。要确保你打算规划为内联的 CSS 对你的网页是真正至关重要的,并且不要把所有 CSS 都内联

找到关键 CSS

手动查找关键 CSS 是一件无聊而艰巨的任务。幸运的是,有工具可以帮助我们快速找到。

使用 Grunt

如果你熟悉 Grunt 构建系统,有一个插件 grunt-critical 可以让这个过程变得更简单点。如果你喜欢用 Gulp 或者 npm,下一节我们会讲如何用这两个工具执行此过程。

首先,需要用如下命令安装该插件:

npm install grunt-critical --save-dev

还需要创建 Gruntfile.js。这个文件包含设置插件的各种选项的所有代码,包括视口大小、源文件和目标文件的路径。这里有一个示例:

module.exports = function(grunt) {

  grunt.initConfig({
    critical: {
      extract: {
        options: {
          base: './',
          width: 1024,
          height: 768,
          minify: true
        },
        src: 'path/to/initial.html',
        dest: 'path/to/final.html'
      }
    }
  });

  grunt.loadNpmTasks('grunt-critical');
  grunt.registerTask('default', ['critical']);

};

在封装的函数中,我们使用 grunt.initConfig 方法指定所有的配置选项。在本例中,我已经指定了一个 base 目录,源文件和目标文件要写到这个目录中。我也将 minify 选项设置为 true,这样我得到的就是一个插件抽取的关键 CSS 的最终压缩版本。src 属性包含了源文件的位置,这个文件要被再次操作。dest 属性包含了最终输出要保存的位置。

如果 dest 文件是一个样式表,最终的 CSS 被保存到一个文件,供将来使用。但是,如果 dest 属性是一个标记文件(HTML、PHP 等等),那么该 CSS 就被内联,已有的样式表被封装进一个 JavaScript 函数中,用于异步加载。对于 disabled 掉 JavaScript 的用户,还会会添加一个 noscript 块。还有其它的选项,你可以访问 Critical 的文档来看看全部选项列表。

现在,在控制台键入 grunt 就可以运行该插件了:

C:\path\to\project>grunt

如果初始文件有如下标记:

<!doctype html>
<html>
  <head>
    <title>The Optimizer</title>
    <link rel="stylesheet" href="link/to/stylesheet">
  </head>
  <body>
     <div>All the markup stuff</div>
  </body>
</html>

现在它就会变成如下这样:

<!doctype html>
<html>
  <head>
    <title>The Optimizer</title>
    <style type="text/css">Minified Inlined CSS</style>
    `<script id="loadcss">`
       JavaScript to load styles asynchronously...
    </script>
    <noscript>
      <link rel="stylesheet" href="link/to/stylesheet">
    </noscript>
  </head>
  <body>
     <div>All the markup stuff</div>
  </body>
</html>

正如你所见,这个插件会为你做完所有工作。现在,如果用 PageSpeed 分析你的页面,会得到更好的分数。在我的例子中,分数从 86 变为 95。

有很多其它 Grunt 插件可以实现相同的效果,比如 grunt-criticalcssgrunt-penthose。但是,在使用这些插件时,必须指定从哪个样式表抽取关键 CSS。

使用 npm 模块

Critical 是由 Addy Osmani 创建的 npm 包,它包含 grunt-critical 插件使用的功能。这个包你可以不与 Grunt 一起用,直接使用 JavaScript 和 npm,用它从一个网页中抽取和内联关键路径或者首屏CSS。要安装这个包,你需要运行:

npm install critical --save-dev

安装完包后,需要在项目目录中创建一个 JavaScript 文件,然后把如下代码放进去:

var critical = require('critical');

critical.generate({
  inline: true,
  base: 'initial/',
  src: 'homepage.html',
  dest: 'final/homepage.html',
  width: 1366,
  height: 768
});

可以指定一堆选项来创建优化的网页。设置 inlinetrue,会生成内联 CSS 的 HTML 页面,否则就生成一个 CSS 文件。widthheight 选项用来设置目标视口的大小。在 critical npm 包的文档上可以找到很多其它选项,比如压缩内联 CSS。优化了的网页与 Grunt 插件的输出看起来会差不多。

可以使用的另一个 npm 模块是 critical-css 模块。该模块从提供的 URL 中生成关键 CSS。生成的 CSS 可以用一个回调函数进一步处理,因为 Grunt 插件就是基于这个包的。

使用 Gulp

如果你觉得用 Gulp 更舒服,Addy Osmanni 推荐 Gulp 用户直接使用 Critical npm 模块。Addy Osmani 在 Critical GitHub page 上为 Gulp 用户提供的示例看起来像这样:

var critical = require('critical').stream;

// Generate & Inline Critical-path CSS
gulp.task('critical', function () {
  return gulp.src('dist/*.html')
    .pipe(critical({base: 'dist/', inline: true, css: ['dist/styles/components.css','dist/styles/main.css']}))
    .pipe(gulp.dest('dist'));
});

Critical 团队还有一个示例 Gulp 项目实战

还有一个 gulp-critical-css 插件可以生成关键 CSS。但是,这个插件是抽取从 CSS 中抽取某些选择器类型,而不是检测首屏元素等等。

更多资源

另一个工具是 Jonas Ohlsson 的 关键路径 CSS 生成器。用这个工具,你只需要键入网页 URL,然后提供你要抽取关键 CSS 的所有 CSS。点击 “Create Critical Path CSS” 按钮,此后就会输出你要的 CSS。

CSS-Tricks 上有篇文章概述了如何使用 CSS 预处理器创建 critical fold CSS。此外,SitePoint 过去也出版了一篇关于优化 critical rendering path的优秀文章,你可以读读这篇文章,更深入地理解此主题。

总结

应该还是不应该使用内联关键 CSS,取决于用户的访问模式,以及网站的结构。如果你的网站页面不多,用户访问也不频繁,或者你已经有一个带有框架和插件的复杂网站,那么内联关键 CSS 可以显著提升性能。

对于内联,唯一不爽的是它会对每次访问加大了页面的重量。但是有办法可以缓解一下,比如使用 cookies 以及在第一次访问时只发送关键 CSS,同时依然异步加载全部 CSS 文件,然后缓存该文件。这样做有点复杂,但是可以得到两全其美的效果。

你已经尝试在自己的项目中内联 CSS 了吗?性能提升明显不?你对同行有其它建议没有?请在评论中让我们知道。

相关文章