De Voorhoede
在 De Voorhoede工作的日子里,我们一直在追寻为用户构建高性能的前端解决方案。不过并不是每个客户会乐于遵循我们的性能指南,以至于我们必须一遍又一遍地跟他们解释那些保证他们能够战胜竞争对手的性能策略的重要性。最近我们也重构了自己的官方主页,使其能够拥有更快地响应速度与更好地性能表 现。

性能调优始于设计
在前端项目中,我们常常与产品经理以及
Static Site Generator
为了演示与测试方便,我们基于
{
"keywords": ["performance", "critical rendering path", "static site", "..."],
"publishDate": "2016-08-12",
"authors": ["Declan"]
}
而其内容为
# A case study on boosting front-end performance
At [De Voorhoede](https://www.voorhoede.nl/en/) we try to boost front-end performance...
## Design for performance
In our projects we have daily discussions...
下面,我们就这个静态网站,进行一些讨论。
Image Delivery
图片是网站的不可或缺的部分,其能够大大提升网站的表现力与视觉效果,而目前平均大小为

WebP
WebP 是面向现代网页的高压缩低损失的图片格式,通常会比
picture
标签
使用
<picture>
<source type="image/webp" srcset="image-l.webp" media="(min-width: 640px)" />
<source type="image/webp" srcset="image-m.webp" media="(min-width: 320px)" />
<source type="image/webp" srcset="image-s.webp" />
<source srcset="image-l.jpg" media="(min-width: 640px)" />
<source srcset="image-m.jpg" media="(min-width: 320px)" />
<source srcset="image-s.jpg" />
<img alt="Description of the image" src="image-l.jpg" />
</picture>
这里我们使用了 picturefill by Scott Jehl 作为
图片多格式生成
现在我们已经可以通过设置不同的图片尺寸、格式来保证图片的分发优化,不过我们总不希望每次要用一张图片的时候就去生成
- 首先是要gulp responsive来生成不同尺寸的图片,该插件同样会输出
WebP 格式的图片 - 压缩生成好的图片
- 用户只需要在
MarkDown 中编写
即可 - 我们自定义的
MarkDown 渲染引擎会在处理过程中自动使用picture 元素替换这些img 标签
SVG Animation
我们的网站中也存在着很多的
-
SVG 是矢量表示,往往比位图文件更小 -
SVG 自带响应式功效,能够根据容器大小进行自动缩放,因此我们不需要再为了picture 元素生成不同尺寸的图片 -
最重要的一点是我们可以使用
CSS 去改变其样式或者添加动画效果,关于这一点可以参考CodePen 上的这个演示。
Custom Web Fonts
我们首先回顾下浏览器是如何使用自定义字体的,当浏览器识别到用户在@font-size
定义的字体时,会尝试下载该字体文件。而在下载的过程中,浏览器是不会展示该字体所属的文本内容,最终导致了所谓的Flash of Invisible Text
现象。现在很多的网站都存在这个问题,这也是导致用户体验差的一个重要原因,即会影响用户最主要的内容浏览这一操作。而我们的优化点即在于首先将字体设置为默认字体,而后在自定义的

首先,我们会为需要使用到的
html {
font-family: Georgia, serif;
}
html.fonts-loaded {
font-family: Noto, Georgia, serif;
}
不过现在font-display
属性也原生提供了我们这种替换功能,更多详情可见font-display属性。
JS 与CSS 的懒加载
总的来说我们希望所有的资源能够尽可能快地加载完毕,不过往往为了保证首页加载的速度,我们会考虑将部分非首屏需要的
Lazy Load JS
目前来说,我们的网站都是偏向于静态,并不需要太多的defer
的属性,这保证了浏览器只会先将该脚本下载下来,然后等到整个页面加载完毕再执行该脚本。另一个需要注意的是,因为我们并不使用类似于
<script>
// Mustard Cutting
if ("querySelector" in document && "addEventListener" in window) {
document.write('<script src="index.js" defer><\/script>');
}
</script>
Lazy Load CSS
正如上文所述,我们的网站偏向于静态展示,因此首屏的最大问题就是

上图中红色的线,即是所谓的折叠分割点。
服务端与缓存
高性能的前端离不开服务端的支持,在我们的实践中也发现不同的服务端配置同样会影响到前端的性能。目前我们主要使用
Configuration
我们首先对于合适的服务端配置做了些调研,这里推荐是使用H5BP Boilerplate Apache Configuration作为配置模板,它是个不错的兼顾了性能与安全性的配置建议。同样地它也提供了面向其他服务端环境的配置。我们对于大部分的
HTTPS
使用
-
设置
HTTP Strict Transport Security 请求头可以让服务端告诉浏览器其只允许通过HTTPS 进行交互,这就避免了浏览器从HTTP 再重定向到HTTPS 的时间消耗。 -
设置
TLS false start 允许客户端在第一轮TLS 中就能够立刻传递加密数据。握手协议余下的操作,譬如确认没有人进行中间人监听可以同步进行,这一点也能节约部分时间。 -
设置
TLS Session Resumption ,当浏览器与服务端曾经通过TLS 进行过通信,那么浏览器会自动记录下Session Identifier ,当下次需要重新建立连接的时候,其可以复用该Identifier ,从而解决了一轮的时间。
这里推荐扩展阅读下 Mythbusting HTTPS: Squashing security’s urban legends by Emily Stark。
Cookies
我们并没有使用某个服务端框架,而是直接使用了静态的
<!-- #if expr="($HTTP_COOKIE!=/css-loaded/) || ($HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css' )"-->
<noscript><link rel="stylesheet" href="0d82f.css" /></noscript>
<script>
(function() {
function loadCSS(url) {...}
function onloadCSS(stylesheet, callback) {...}
function setCookie(name, value, expInDays) {...}
var stylesheet = loadCSS('0d82f.css');
onloadCSS(stylesheet, function() {
setCookie('css-loaded', '0d82f', 100);
});
}());
</script>
<style>
/* Critical CSS here */
</style>
<!-- #else -->
<link rel="stylesheet" href="0d82f.css" />
<!-- #endif -->
这里<!-- #
,其主要包含以下步骤
-
$HTTP_COOKIE!=/css-loaded/
检测是否有设置过CSS 缓存相关的Cookie -
$HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css'
检测缓存的CSS 版本是否为当前版本 -
If <!-- #if expr="..." -->
值为 true
我们便能假设该用户是第一次访问该站点 -
如果用户是首次浏览,我们添加了一个
<noscript>
标签,里面还包含了一个阻塞型的<link rel="stylesheet">
标签。添加该标签的意义在于我们在下面是使用JavaScript 来异步加载CSS 文件,而在用户禁止JavaScript 的情况下也能保证可以通过该标签来正常加载CSS 文件。 -
<!-- #else -->
表达式在用户二次访问该页面时,我们可以认为CSS 文件已经被加载过了,因此可以直接从本地缓存中加载而不需要重复请求。
上述策略同样可以应用于

File Level Caching
在上文可以发现,我们严重依赖于浏览器缓存来处理用户重复访问时资源加载的问题,理想情况下我们肯定希望能够永久地缓存https://www.voorhoede.nl/assets/css/main.css?v=1.0.4
形式,即在请求路径上加上版本号的方式进行缓存。不过这种方式的缺陷在于如果我们更换了资源文件的存放地址,那么所有的缓存也就自然失效了。这里我们使用了 gulp-rev 以及 gulp-rev-replace 来为文件添加
Result
上面我们介绍了很多的优化手段,这里我们以实验的形式来对优化的结果与效果进行分析。我们可以用类似于 PageSpeed Insights 或者 WebPagetest 来进行性能测试或者网络分析。我觉得最好的测试你站点渲染性能的方式就是在限流的情况下观察页面的呈现效果,

这里我们将我们的网络环境设置为了

而在

Roadmap
优化之路漫漫,永无止境,我们在未来也会关注以下几个方面:
-
HTTP/2: 我们目前已经开始尝试使用HTTP/2 ,而本篇文章中提到的很多的优化的要点都是面向HTTP/1.1 的。简言之,HTTP/1.1 诞生之初还是处于Table 布局与行内样式流行的时代,它并没有考虑到现在所面对的2.6MB 大小,包含200 多个网络请求的页面。为了弥合这老的协议的缺陷,我们不得不连接JS 与CSS 文件、使用行内样式、对于小图片使用Data URL 等等。这些操作都是为了节约请求次数,而HTTP/2 中允许在同一个TCP 请求中进行多个并发的请求,这样就会允许我们不需要再去进行大量的文件合并操作。 -
Service Workers: 这是现代浏览器提供的后台工作线程,可以允许我们为网站添加譬如离线支持、推送消息、后台同步等等很多复杂的操作。 -
CDN: 目前我们是自己维护网站,而在真实的应用场景下可以考虑使用CDN 服务来减少服务端与客户端之间的物理距离,从而减少传输时延。