Post

Jekyll 网站性能优化

近来,突然觉博客项目的 CSS 内容越来越多,多次在 HTML 与 CSS 之间增减内容,就会增加残留无效 CSS 的概率,人工筛除颇为费神。经过查找资料,发现 Chrome 的 DevTools 自带的 Coverage 功能可以很好的解决这个问题。好奇心驱使下,自然想着寻找更多玩法,能不能借助 DevTools 提高网站性能呢?答案是肯定的。Lighthouse 是 Chrome 的性能审查扩展程序,位置在 DevTools 的 Audits 选项栏。根据自动化审查结果,可以知道网站性能短板所在,再由报告提供的建议去优化缺陷。

Lighthouse 审查

devtool-audits

Audits 有五项审查内容,分别是 PerformanceProgressive Web AppBest parcticesAccessibilitySEO 等,可以根据自己的需要选择。本文针对其中的 Performance(性能)开聊。Chrome 打开目标网页,启动 Audits,十几秒内即可完成审查。 网站的性能优化大概有以下几个方向:

  • 跨域资源预处理
  • JS 异步加载
  • 按业务分拆 JS/CSS 的调用
  • 本地资源压缩
  • 图片懒加载

下面将逐项叙述每个优化过程细节。

跨域资源预处理

对于跨域的静态资源引用,譬如字体、图标资源的引用,在 <link> 标签中使用属性 preconnectdns-prefetch 可以告诉浏览器对跨域资源给予最高优先级,在页面加载一开始就进行处理。

dns-prefetch
告诉浏览器在后台自动获取目标网址的 DNS 信息,这样在页面使用所需资源时就可以减少网络请求的时间。
preconnect
更进一步,除了完成上述工作,还进行 TLS 判断以及 TCP 三次握手,是更为重量级的操作。示例:
1
<link href="https://cdn.domain.com" rel="preconnect" crossorigin>

实际应用时,两者会合并使用:

1
2
<link href="https://cdn.domain.com" rel="preconnect" crossorigin>
<link href="https://cdn.domain.com" rel="dns-prefetch">

两者合用,可以减少后续的跨域延迟。另外,对于不支持 preconnect 的浏览器引擎,还有 dns-prefetch 作后备,dns-prefetch 对浏览器种类和版本的支持比前者要更广1

JS 加载优化

默认情况下,DOM 解析和 JS 执行 会同步进行,当浏览器解析到 <script> 标签时,会阻塞 DOM 树的解析,直至 script 内容下载完毕并执行,才会继续 DOM 树的构建 2

gantt
  dateformat ss
  axisformat %S ms

  title  Basic situation

  section HTML
  build DOM: active, dom, 00, 20s
  build DOM: active, dom-2, after js-run, 10s

  section JS
  fetch script: js-dl, after dom, 10s
  run script: crit, js-run, after js-dl, 5s

浏览器之所以赋予 <script> 标签最高优先级,是因为 JS 代码可以修改 HTML/CSS 的结构及内容,从而对最终渲染结果发生影响,所以必须等候它执行完成才会继续构建 DOM/CSS 树。

JS 资源的下载可以和 DOM 树构建异步进行,从而减少或者避免 JS 代码增加页面加载时长。实现方式为:对 <script> 使用属性 asyncdefer。请注意异步属性只能在外部调用的 JS 资源里使用,即 <script> 标签中必须提供 src 属性。

Async 属性

对页面样式展示非必要的 JS 代码,可以在 <script> 内部添加属性 async 来异步下载 script 内容,减少对 DOM 构建的阻塞。如:

1
<script src="/assets/js/common.js" async></script>

即便使用 async 还是会对 DOM 树解析造成阻塞,因为 JS 内容下载完毕后,会被立即执行,若此时 DOM 解析还没完成,就会被阻塞3:

gantt
  dateformat ss
  axisformat %S ms

  title With async

  section HTML
  build DOM: active, dom, 00, 20s
  build DOM: active, dom2, after js-run, 10s

  section JS
  fetch script:  js-dl, 10, 10s
  run script: crit, js-run, after js-dl, 5s

另外,还有个特点,如果同时存在几个 async 的 JS 链接,那么它们被执行的次序是随机的,不受控制。

Defer 属性

如果要保障 JS 放在 DOM 完成解析后执行,可以使用 defer 属性:

gantt
  dateformat ss
  axisformat %S ms

  title  With defer

  section HTML
  build DOM: active, dom, 00, 30s

  section JS
  fetch script: js-dl, 10, 10s
  run script: crit, js-run, after dom, 5s

defer 还有一个与 async 不同的地方是:当多个 defer 资源同时存在时,它们会依照声明次序被执行。

分场景调用 JS/CSS

在 Jekyll 中,{% include %} 的应用可以增加代码的简洁性,也可能造成页面的 JS/CSS 冗余。一般 Jekyll 项目的习惯是,项目所有的 JS/CSS 都在 _includes/head.html 中引入,然后通过 {% include head.html %} 导入每个下层 layout。很多时候,部分 JS/CSS 并不是每个页面都必须的。

例如,本站的 post 布局需要 bootstrap-toc.js,此外的 page 布局,home 布局都不需要 toc 相关的 JS/CSS。 这时应该把 bootstrap-toc 相关的 JS/CSS 单独放置到 post 布局上引用,从而增加了其他布局的加载及渲染速度。

本地资源压缩体积

SASS 压缩

Jekyll 提供的 SASS Pipeline 可以把存放在 _sass 目录下的文件自动压缩,使用时只需在项目 _config.yml 文件添加 SASS 配置:

1
2
sass:
  style: compressed

接着就可以在汇总 SCSS 文件导入所需的文件。例如,在文件 assets/css/styles.scss 里面引入 _sass/main.scss_sass/syntax.scss 两个文件,styles.scss 应添加以下内容:

1
2
3
4
5
---
---

@import "main";
@import "syntax";

JS 压缩

项目中自己编写的 JS 以及第三方的 JS 文件(未压缩),可使用 YUI Compressor 压缩,压缩结果命名可以在 .js 后缀前添加 min 标识。如原文件 tools.js 压缩后命名为 tools.min.js

1
$ java -jar yuicompressor.jar tools.js -o tools.min.js

接着使用 Jekyll 的 Assest 特征合并引用所需的 JS,在 JS 目录下新建文件 common.js,使用 {% include %} 引入其他 JS,如本站内容为:

1
2
3
4
5
6
7
---
---

{% include_relative dist/back-to-top.min.js %}
{% include_relative dist/category-collapse.min.js %}
{% include_relative dist/search-display.min.js %}
{% include_relative dist/sidebar-toggle.min.js %}

项目 HTML 的 head 中使用合并文件替代分开的几个文件引用:

1
<script src="/assets/js/common.js" async></script>

HTML 压缩

对于 Jekyll,有一个 Liquid 实现的开源方案提供解决:Jekyll HTML Compressor

安装很简单,首先到 Release 下载最新版的 compress.html,拷贝到项目的 _layouts 目录下。然后修改最顶级的默认样式 _layouts/default.html,在它的头部添加 YAML:

1
2
3
---
layout: compress
---

然后,在 _config.yml 添加配置:

1
2
3
4
5
6
7
8
compress_html:
  clippings: all
  comments: ["<!-- ", " -->"]
  endings: [html, head, body, li, dt, dd, rt, rp, optgroup, option, colgroup, caption, thead, tbody, tfoot, tr, td, th]
  profile: false
  blanklines: false
  ignore:
    envs: []

具体每个参数的含义可以参考项目 README 介绍,经过几步即完成了插件的安装。该注意的是,如果 post 使用了 Jekyll 的 linenos,则需要额外解决内嵌 <pre> 压缩错乱的 BUG

具体表现为,原始状态使用 {% highlight LANGUAGE linenos %} 时,生成的代码格式为:

1
2
3
4
5
6
7
<figure class="highlight">
  <pre>
    <code class="..." data-lang="...">
     Code snippet
    </code>
  </pre>
</figure>

当压缩 HTML 时,会把最外层的 <pre></pre> 删除,但是不完整,残留 />,见下列码第二行:

1
2
3
4
5
6
<figure class="highlight">
  />
  <code class="..." data-lang="...">
    Code snippet
  </code>
</figure>

即使这会造成页面 HTML 结构大错乱,但是项目维护者不打算修改这个 bug:他们认为这样会增加对 HTML 扫描的层数,大大增加项目 build 的时间。上述 issue#71 中,已经有用户提供了解决方法:

_includes/ 中新建文件 fixlinenos.html,添加内容:

1
2
3
4
{% if _code contains '<pre class="lineno">' %}
  {% assign _code = _code | replace: "<pre><code", "<code" %}
  {% assign _code = _code | replace: "</code></pre>", "</code>" %}
{% endif %}

在每篇 post 源码中,调用 {% highlight LANGUAGE linenos %} 的地方作出修改。

原始代码:

1
2
3
{% highlight AnyLanguage linenos %}
  Some code
{% endhighlight %}

应修改为:

1
2
3
4
5
6
{% capture _code %}
{% highlight AnyLanguage linenos %}
  Some code
{% endhighlight %}
{% endcapture %}
{% include fixlinenos.html %}{{ _code }}

这样压缩就不会出现问题了。

此外,Jekyll HTML Compressor 这个插件还有个局限:对于内嵌在 HTML 页面的 JavaScript 代码,注释若使用 // 会导致压缩错误,需要用 /* */ 替代。

JSON 压缩

这个是个意外发现,和上一步 HTML 压缩 一样,文件头部加 YAML 声明调用压缩的 layout 即可:

1
2
3
---
layout: compress
---

服务器 GZIP 压缩

传输过程可以开启 Web 服务器的 GZIP 选项,如果博客是部署在 GitHub Pages 或 Coding Pages,则默认开启了 GZIP 压缩传输。若是自己个人部署的 Web 服务器,如 Nginx,Apache 等,则需要跟进此点优化。

懒加载图片

图片是重要的内容资源,但是却不能忽略图片体积对网络带宽带来的负面影响。解决的策略就是对无需立即展示的图片,进行懒加载,也就是当网页滑动到图片所在位置时才进行加载。这个过程可以通过第三方库来完成,如 Lozad.js,轻量级,支持自定义加载过程细节。

结语

上述只是针对本站的优化,并没有覆盖常规网站的所有优化点,更加全面细致的性能建议,可参考 Google 开发者文档

参考资料

引用

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.