Jekyll 网站性能优化
近来,突然觉博客项目的 CSS 内容越来越多,多次在 HTML 与 CSS 之间增减内容,就会增加残留无效 CSS 的概率,人工筛除颇为费神。经过查找资料,发现 Chrome 的 DevTools 自带的 Coverage
功能可以很好的解决这个问题。好奇心驱使下,自然想着寻找更多玩法,能不能借助 DevTools 提高网站性能呢?答案是肯定的。Lighthouse
是 Chrome 的性能审查扩展程序,位置在 DevTools 的 Audits
选项栏。根据自动化审查结果,可以知道网站性能短板所在,再由报告提供的建议去优化缺陷。
Lighthouse 审查
Audits
有五项审查内容,分别是 Performance
,Progressive Web App
,Best parctices
,Accessibility
,SEO
等,可以根据自己的需要选择。本文针对其中的 Performance
(性能)开聊。Chrome 打开目标网页,启动 Audits
,十几秒内即可完成审查。 网站的性能优化大概有以下几个方向:
- 跨域资源预处理
- JS 异步加载
- 按业务分拆 JS/CSS 的调用
- 本地资源压缩
- 图片懒加载
下面将逐项叙述每个优化过程细节。
跨域资源预处理
对于跨域的静态资源引用,譬如字体、图标资源的引用,在 <link>
标签中使用属性 preconnect
和 dns-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>
使用属性 async
或 defer
。请注意异步属性只能在外部调用的 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 开发者文档。
参考资料
- Building the DOM faster: speculative parsing, async, defer and preload
- Resource Hints - What is Preload, Prefetch, and Preconnect?
- Resource Prioritization – Getting the Browser to Help You
- async and defer