提高前端性能的一个案例
在De Voorhoede,我们尽所能地为客户推进前端性能。虽然不是很容易就能说服客户完全按照我们的指令去做,但我们一直在尽全力和他们沟通,解释提高性能的重要性,或者将他们的性能与他们主要竞争对手进行比较。
最近更新了我们的网站,除了设计上进行了大修,这也是一个将性能提高到最优的理想机会。我们的目标是有效控制,注重性能,与未来技术兼容,并将创建网站变为了一件有趣的事。以下是我们如何改进网站的具体做法。
性能设计
在我们的项目中,我们每天都要和设计师和产品负责人讨论如何平衡美学和性能。对于我们自己的网站,这就变得很简单了。我们都相信一个好的用户体验都是从尽快响应显示内容开始的。这就意味着性能重于美学。
好的内容,布局,图像和交互性对于吸引你的受众群体是必不可少的,但这些元素都对页面加载时间和最终用户体验都有影响。在每一步,我们都在探究如何才能在对性能影响最小化的基础上得到良好的用户体验和设计。
内容先行
我们希望核心内容(也就是在重要的HTML和CSS文件中的内容)能最先出现在我们的访客面前。相比于效果增强,JavaScript,完备的CSS,网页字体,图像和分析等,核心内容的显示应该为最先级。
完全的控制
为我们理想的网站制定了标准之后,我们的结论是,我们需要完全控制网站的每一个方面。所以我们选择建立我们自己的静态站点生成器。
静态站点生成器
我们使用Node.js
创建静态站点生成器。它采用短JSON页面元描述的Markdown文件生成包含其全部资产的完整的网站结构。它也可以由一个HTML文件包括特定页面的JavaScript伴随。
以下是用来生成真正的HTML的简化的元描述和Markdown文件:
JSON文件:
{
"keywords": [
"performance",
"critical rendering path",
"static site",
"..."
],
"publishDate": "2016-08-12",
"authors": ["Declan"]
}
Markdown文件:
# A case study on boosting front-end performance
At [De Voorhoede](https://www.voorhoede.nl/en/) we try to boost front-end performance...
图像传输
网页平均字节高达2406kb
,其中1535kb
是图像。可见图像占据了网页极大的一部分,所以它是提高网页性能的很好的指标之一。
webP
WebP是一个现代化的图像格式,提供了卓越的图像无损和有损压缩。webP格式的图片比其他格式的图片大大缩小:有时他能比同样的JPEG格式的图片小25%
。但是webP却经常被忽略并很少被使用。在我写这篇文章的时候,仅仅只有Chrome, Opera 和 Andriod支持webP格式(但是这仍然超过了50%
的用户)。同时我们也可以优雅降级到JPG/PNG格式。
<picture>
元素
使用<picture>
元素,我们可以将webP优雅降级到更加广泛支持的图片格式,比如JPEG格式:
<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>
我们使用了@Scott Jehl写的picturefill来解决浏览器兼容的问题,最终在所有浏览器都获得一致的行为。
我们使用<img>
来作为不支持<picture>
和(或)不支持JavaScript浏览器的后备选择。
生成
虽然图像传输方法是合适的,我们仍然必须弄清楚如何无痛实现它。我喜欢<picture>
元素,但我讨厌写上面的代码片段,尤其是在写内容的时候,我不希望每次都被需要书写图片的6
个实例而困扰。所以我们在Markdown文档中优化图片和完成<picture>
元素的代码片段。所以我们:
- 在创建的过程中生成源图片不同格式的版本,包括JPEG,PNG和webP格式。我们使用gulp responsive来完成这项工作。
- 压缩生成的图片。
- 在我们的Markdown文件中添加
![Description of the image](image.jpg)
。 - 在创建过程中使用自定义编写的Markdown来渲染生成
<picture>
元素。
SVG动画
我们为网站选择了一个独特的图形样式,其中SVG图像发挥了重要的作用。这么做主要有这几个原因:
- 首先,SVG是矢量图,比位图更小;
- 其次,SVG图片能在不同尺寸大小的设备上做到很好的缩放。省去了图片生成器和
<picture>
元素。 - 最后,可以通过CSS来改变SVG格式的图片甚至添加动画。这是性能设计的一个完美例子。在我们的产品页面有一个自定义SVG动画,在简介页面也重复利用了它。可重复使用的特点保证了设计的一致性,并且对性能的影响几乎可忽略不计。
自定义网页字体
在深入之前,先简单了解一下与网页字体有关的知识。当我们在CSS中使用@font-face
指令,如果用户的浏览器没有我们使用的字体,浏览器就会自己下载字体文件。但是在下载的时候,大部分浏览器并不能使用该字体显示文字直到下载完成。这种情况被称为FOIT(Flash of Invisible Text)。这种情况极大破坏了终端的用户体验。而这直接影响了我们的核心目标:最快地传达内容。
不过我么可以强制性改变浏览器的行为。告诉浏览器先使用最普通的字体,比如Arial
或者Georgia
来显示内容,一旦浏览器下载字体文件结束,再替换之前的字体并重新渲染所有文字内容。因此,如果下载字体失败,网页内容仍然是可读的。我们认为网站自定义字体是一种增强效果,所以即使没有也不应该影响网站的正常工作。
使用自定义网页字体能提升用户体验,但不要忘记优化它以保证不影响网站的正常工作。
字体子集
使用字体子集是最佳的提高性能的方式,我会毫不犹豫地向任何使用自定义网页字体的开发者推荐这个方法。如果你对内容有完全的控制并很确定会使用哪些字符,那么请一定要使用字体子集。即使你只是选择西方字体作为子集,相比于我们平时默认的大小能达到246KB
的WOFF
字体,西方字体的子集大小会大幅度减小到31KB
。我们使用的是Font squirrel webfont generator来生成我们需要的字体子集。
Font face observer
@Bram Stein的Font face observer是一款检查字体是否加载的插件。当这个插件通知我们字体下载完成之后,我们在CSS文件中将fonts-loaded
添加到<html>
元素上。
html {
font-family: Georgia, serif;
}
html.fonts-loaded {
font-family: Noto, Georgia, serif;
}
注意:我之前并没有将Noto的@font-face
声明添加到CSS文件中。
在浏览器的缓存中我们还设置了一个cookie
来记住字体已经加载。这一点我之后会进一步解释。
在未来我们很可能不再使用Bram Stein的Font face observer插件,因为CSS最近已经拟定一个新的@font-face
描述符(叫做font-display
),他的属性值将会控制字体被加载前后将如何渲染。CSS指令为font-display: swap;
的作用将和我们现在实现的一样。这里可以看到更多有关font-display
的内容。
延迟加载JS和CSS
一般来说,我们有尽快加载资源的方法。我们清除渲染阻塞请求并优化首次浏览,利用浏览器缓存来重复浏览。
延迟加载JS
一开始,我们并没有在网站上使用很多的JavaScript。但是为了之后的开发,我们开发了一套JavaScript的工作流程。
JavaScript是在<head>
模块渲染的,但是这个结果并不是我们想要的。因为JavaScript的作用应该只是为了增强用户体验,对于访客来说并不是必需的。最简单修复这个问题的方法就是将JavaScript放到网站HTML文件的末尾处。但这样做的不足之处在于只有在HTML下载完成之后才会开始加载脚本。
另一种方法是在脚本放在头部并通过在<script>
内添加defer
属性来推迟脚本的加载。这使得脚本无阻塞的浏览器几乎是立即下载它,而不直到页面加载完才执行代码。
还有一点需要提及,我们还使用类似于jQuery这样的库,我们只需要加载JavaScript的浏览器能支持某些功能,比如mustard cutting。最终代码如下:
<script>
// Mustard Cutting
if ('querySelector' in document && 'addEventListener' in window) {
document.write('<script src="index.js" defer><\/script>');
}
</script>
我们在网页头部写了一小行脚本来判断浏览器是否支持document.querySelector
和window.addEventListener
。如果支持那就加载JS文件,并通过defer
属性来延迟加载。
延迟加载CSS
对于第一次浏览,最大的渲染阻塞来自于CSS文件。浏览器延迟了页面的渲染直到所有网页头部的CSS文件都下载并解析完成。此行为是故意的,否则浏览器将需要在渲染的所有时间内重新计算布局和重绘。
为了防止CSS文件造成渲染阻塞,需要异步加载CSS文件。我们使用了@Filament Group的loadCSS function来实现异步加载。当CSS加载完成时它会给你一个反馈,同样我们也设置了cookies
来保存这个状态。
但是异步加载CSS文件有一个“问题”。HTML加载速度极快,在CSS没有加载完成之前,HTML文件没有任何CSS渲染。为了避免这种情况,我们引入了Critical CSS。
Critical CSS
Critical CSS被描述为渲染首屏所需的最小CSS集合。我们专注于“在首屏”的内容。显然,不同设备之间首屏的位置也有很大的不同,我们只能尽可能做出最佳猜测。
手动确定Critical CSS无疑是一个耗时的过程,这里提供了几个脚本可在你开发过程中生成Critical CSS。我们使用的是@Addy Osmani写的critical npm module来自动生成Critical CSS.
下面是我们使用Critical CSS渲染和使用完整的CSS渲染的对比的图。可以看出,使用Critical CSS渲染的网站折叠部分还没有加载样式。
服务器
De Voorhoede网站是我们自己设置的,因为希望能控制服务器环境。同时也希望能尝试改变服务器配置来提高网站性能。我们有一个Apache Web服务器,并通过HTTPS协议创建网站。
配置
为了提高性能和安全性,我们对如何配置服务器进行了一些研究。
我们使用H5BP boilerplate Apache configuration进行配置,这是一个提高服务器性能和安全的很好开头。他们也有其他服务器环境的配置。
我们把绝大多数的HTML,CSS和JavaScript文件压缩为GZIP。并为所有资源设置了缓存头文件。我会在之后进一步说明。
HTTPS
采用HTTPS协议将对你的网站性能有一些影响。性能损失主要来自于建立SSL握手引入的大量延迟。但是,同样的,我们还是可以做一些事来弥补这些性能损失。
HTTP Strict Transport Security 是一个告诉服务器浏览器应该只能使用HTTPS进行通信的HTTP头域。这样,它可以防止被重定向到HTTPS的HTTP请求。使用HTTP访问该站点的所有努力应自动进行转换。这为我们节省了往返的时间!
TLS false start 允许客户端在首次TLS往返后立刻开始传输加密数据。这种优化降低了新TLS连接一个往返的握手开销。一旦客户知道加密密钥就可以开始传输应用程序数据。握手的其余部分是花费在确认没有人篡改握手记录,并且可以并行完成。
TLS session resumption 节省了另一个往返,如果浏览器和服务器在过去传达了TLS,确保浏览器可以记住会话标识符,当下一次建立连接的时候,该标识符就可以重复使用,从而节省了一次往返。
以上的内容我都是通过查看资料和视频得来的,如有不妥之处请和我联系。另外极力推荐Mythbusting HTTPS: Squashing security’s urban legends by Emily Stark这个视频。
使用Cookies
我们没有一个服务器端语言,只有一个静态的Apache Web服务器。但是,Apache Web服务器仍然可以做服务器端来包含(SSI)和读出cookies。通过智能cookies的使用,并由Apache重写服务于HTML的部分,我们就可以提高前端性能。具体你可以参考以下的代码:
<!-- #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 -->
Apache服务器端逻辑是寻找以<!-- #
开头的行。下面我们一步一步来解读这段代码:
$HTTP_COOKIE!=/css-loaded/
检查是否没有保存CSS缓存的cookies存在。$HTTP_COOKIE=/.*css-loaded=([^;]+);?.*/ && ${1} != '0d82f.css'
检查该缓存的CSS版本是不是当前版本。- 如果
<!-- #if expr="..." -->
是“true”,我们认为访客是第一次访问。 - 对于首次浏览我们我们在渲染
<link rel="stylesheet">
模块的时候添加一个<noscript>
标签。我们这样做,是因为我们将使用JavaScript异步加载完整的CSS。但如果JavaScript被禁用,CSS将不能被加载。所以我们添加<noscript>
标签作为后备方案。 - 我们添加了一个内嵌脚本来延迟加载CSS文件,一个
onloadCSS
回调并设置cookies。 - 在同一个脚本中我们异步加载了完整的CSS文件。
- 在
onloadCSS
回调函数中我们将cookies设置为以版本号为值的哈希表。 - 在这段脚本之后我们加载了Critical CSS,这会导致阻断,但是这将是非常小的阻塞,并能防止页面被显示为纯无样式的HTML。
<!-- #else -->
声明(意味着css-loaded
cookies已经存在)代表访客是重复浏览。因为我们几乎可以推断CSS文件之前已经被加载了,所以我们可以利用浏览器的缓存立刻加载CSS文件。
字体的加载我们也采用了同样的方式,首次浏览采用异步加载,重复浏览就利用浏览器的缓存。下图就是我们使用的cookies:
文件缓存
由于在重复浏览网页的时候,很大程度上需要依赖浏览器的缓存,所以需要确保正确缓存了文件。理想情况下,希望能永久缓存资源(包括CSS,JS,字体,图片),仅在文件改变时才使缓存无效。当请求的URL是独一无二的时候,缓存是无效的。当发布一个新版本的时候,git tag
我们的网站,因此最简单的方法就是增加一个带有代码库版本号为查询参数的请求URLs。比如:https://www.voorhoede.nl/assets/css/main.css?v=1.0.4
。
这种方法的缺点是当写一篇新的博客文章时(这是代码库中的一部分),尽管那些资源都没有发生改变,但是缓存都会变得无效。
当我们努力尝试去改进我们的方法的时候,我们无意间发现了gulp-rev和gulp-rev-replace。这些脚本可以帮助我们通过添加内容为文件名的哈希表来添加每个文件的新版本。这就意味着只要文件内容不改变,请求的URL就不会改变。
结果
如果你能看到这里,你很可能会很想知道结果。你可以使用类似于PageSpeed Insights和WebPagetest来测试你的网站性能。我认为测试你网站渲染性能的最好方法是在疯狂节流的情况下观察你的网页的变化。这就意味着你需要以一种不切实际的方法进行节流。在谷歌浏览器中,你可以通过the inspector > Network tab
来限制连接速度,看看在网站建立的时候请求是如何慢慢加载的。
下面是当限制连接速度为50KB/s
的时候我们网站的加载情况:
图片是在2.27s
时页面的加载情况。图中的黄线说明HTML文件已经下载了。而HTML文件中包含了Critical CSS文件,这确保了网页看上去是可用的。而其他阻塞的资源将延迟下载,当所有资源都加载完毕后,我们就可以和页面进行交互了。这正是我们想要的结果。
另一个值得注意的地方是,在如此慢的连接速度的情况下,自定义字体将永远不会被加载。Font face observer会自动处理这种情况,但是如果我们不异步加载字体,大多数浏览器都会出现一会儿FOIT现象。
我们观察到完整的CSS文件将会在8s
后开始下载。如果我们使用阻塞的方式加载完整的CSS文件而不是使用内联Critical CSS文件的方式,那么我们将会在这8s
内都盯着一个白色的页面。
如果你好奇那些不注重性能的网站加载时间会是多少,你完全可以去尝试,你会发现时间将长得突破天际。
最后我们使用之前提及的测试工具测试一下我们的网站。PageSpeed Insights给了我们100/100的分数,简直完美!
我们再看一下WebPagetest的测试结果:
我们可以看到,我们的服务器运行良好,并且第一次浏览的SpeedIndex是693
.这意味着693ms
后我们的页面就可用了。这简直棒极了!
未来规划
我们仍然在不断改进我们的网站。未来我们将集中于以下几个方面:
- HTTP/2: 目前我们仍处于尝试阶段。本文所提到的最佳实践都是基于HTTP/1.1的。HTTP/1.1诞生之初主要是应用于web端内容获取,那时候内容还不像现在这样丰富,排版也没那么精美,用户交互的场景几乎没有。对于这种简单的获取网页内容的场景,http表现得还算不错。但随着互联网的发展和Web2.0的诞生,更多的内容开始被展示(更多的图片文件),排版变得更精美(更多的CSS),更复杂的交互也被引入(更多的JavaScript)。为了减小请求次数我们做了很多工作,但是随着HTTP/2的发展,多路复用的实现使得多个请求可以使用同一个TCP连接,降低了延迟同时提高了带宽的利用率。
- Service Workers: Service Workers是一个现代浏览器的JavaScript API,在后台运行,为实现一些不依赖页面或者用户交互的特性打开了一扇大门。在未来这些特性将包括推送消息,背景后台同步,
geofencing
(地理围栏定位)等等。 - CDN: 现在我们想要转移到一个CDN摆脱由于客户端和服务器之间的物理距离引起的网络延迟。
本文根据@Declan Rek的《A Case Study on Boosting Front-End Performance》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://css-tricks.com/case-study-boosting-front-end-performance
如需转载,烦请注明出处:https://www.fedev.cn/performance/case-study-boosting-front-end-performance.htmlNike Bonafide