从案例分析如何优化前端性能

559 查看

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

性能调优始于设计

在前端项目中,我们常常与产品经理以及UI设计讨论如何在美感与性能之间达到平衡,我们坚信更快地内容呈现是好的用户体验的不可分割的一部分。在我们自己的网站中,我们是以性能优于美感。好的内容、布局、图片与交互都是构成你网站吸引力的不可或缺的部分,不过这些复杂的元素的使用往往也意味着页面加载速度的增加。设计的核心即在于决定我们网站需要呈现哪些内容,往往这里的内容会指图片、字体这样的偏静态的部分,我们首先也从对于静态内容的优化开始。

Static Site Generator

为了演示与测试方便,我们基于NodeJS搭建了一个混合使用MarkDown与JSON作为配置的静态网站生成器,其中一个简单的博客类型的网站的配置信息如下:

而其内容为:

下面,我们就这个静态网站,进行一些讨论。

Image Delivery

图片是网站的不可或缺的部分,其能够大大提升网站的表现力与视觉效果,而目前平均大小为2406KB的网页中就有1535KB是图片资源,可见图片占据了静态资源多么大的一个比重,这也是我们需要重点优化的部分。
1585402170-57bfe50058e0b_articlex

WebP

WebP 是面向现代网页的高压缩低损失的图片格式,通常会比JPEG小25%左右。然后WebP目前被很多人忽视,也不常使用。截止到本文撰写的时候,WebP目前只能够在Chrome, Opera and Android (大概占用户数的 50%)这些浏览器中使用,不过我们还是有办法以JPG/PNG来弥补部分浏览器中不支持WebP的缺憾。

picture标签

使用picture标签可以方便的对于WebP格式不支持的情况下完成替换:

这里我们使用了 picturefill by Scott Jehl作为Polyfill库来保证低版本的浏览器中能够支持picture标签,并且保证跨浏览器的功能一致性。并且我们还使用了img标签来保证那些不支持picture的浏览器能够正常工作。

图片多格式生成

现在我们已经可以通过设置不同的图片尺寸、格式来保证图片的分发优化,不过我们总不希望每次要用一张图片的时候就去生成6个不同的尺寸/实例。我们希望有一种抽象的方法可以帮我们自动完成这一步,为我们自动生成不同的格式/尺寸,然后自动插入合适的picture元素,在我们的静态网站生成器中是这么做的:

  • 首先是要gulp responsive来生成不同尺寸的图片,该插件同样会输出WebP格式的图片
  • 压缩生成好的图片
  • 用户只需要在MarkDown中编写![Description of the image](image.jpg)即可
  • 我们自定义的MarkDown渲染引擎会在处理过程中自动使用picture元素替换这些img标签

SVG Animation

我们的网站中也存在着很多的Icon以及动画性质图片,这里我们是选择SVG作为Icon与Animation的格式,主要考虑有下:

  • SVG是矢量表示,往往比位图文件更小
  • SVG自带响应式功效,能够根据容器大小进行自动缩放,因此我们不需要再为了picture元素生成不同尺寸的图片
  • 最重要的一点是我们可以使用CSS去改变其样式或者添加动画效果,关于这一点可以参考CodePen上的这个演示

Custom Web Fonts

我们首先回顾下浏览器是如何使用自定义字体的,当浏览器识别到用户在CSS中基于@font-size定义的字体时,会尝试下载该字体文件。而在下载的过程中,浏览器是不会展示该字体所属的文本内容,最终导致了所谓的Flash of Invisible Text现象。现在很多的网站都存在这个问题,这也是导致用户体验差的一个重要原因,即会影响用户最主要的内容浏览这一操作。而我们的优化点即在于首先将字体设置为默认字体,而后在自定义的Web Font下载完毕之后对标准字体再进行替换操作,并且重新渲染整个文本块。而如果自定义的字体下载失败,整个内容还是能保证基本的可读性,不会对用户体验造成毁灭性的打击。
2928541744-57bfe509d7def_articlex

首先,我们会为需要使用到的Web Fonts创建最小子集,即只将那些需要使用的字体提取出来,而并不需要让用户下载整个字体集,这里推荐使用Font squirrel webfont generator。另外,我们还需要为字体的下载设置监视器,即保证能够在字体下载完毕之后自动回调,这里我们使用的是fontfaceobserver,它会为页面自动创建一个监视器,在侦测到所有的自定义Web Fonts下载完毕后,会为整个页面添加默认的类名:

不过现在CSS的font-display属性也原生提供了我们这种替换功能,更多详情可见font-display属性。

JS 与 CSS 的懒加载

总的来说我们希望所有的资源能够尽可能快地加载完毕,不过往往为了保证首页加载的速度,我们会考虑将部分非首屏需要的JS/CSS文件进行延迟加载,或者对于重复的视图使用浏览器本地缓存。

Lazy Load JS

目前来说,我们的网站都是偏向于静态,并不需要太多的JavaScript介入,不过考虑到日后的扩展空间,我们还是构建了一套完整的JS的工作流。众所周知,如果将JS直接放置到head标签中,其会阻塞整个页面的渲染。对于该点,最简单的方式就是将会阻塞渲染的JS脚本移动到页面的尾部,在整个首屏渲染完毕之后再进行加载。另一个常用的手段就是依然保持JS文件位于head标签中,不过为其添加一个defer的属性,这保证了浏览器只会先将该脚本下载下来,然后等到整个页面加载完毕再执行该脚本。
另一个需要注意的是,因为我们并不使用类似于jQuery这样的第三方依赖库,而更多的依赖于浏览器原生的特性,因此我们希望在合适的浏览器内加载合适版本的JS代码,其效果大概如下:

Lazy Load CSS

正如上文所述,我们的网站偏向于静态展示,因此首屏的最大问题就是CSS文件的加载问题。浏览器会在head标签中声明的所有CSS文件下载完毕之前一直处于阻塞状态,这种机制很是明智的,不然的话浏览器在加载多个CSS文件的时候会进行重复的布局与渲染,这更是对于性能的浪费。
为了避免非首屏的CSS文件阻塞页面渲染,我们使用loadCSS这个小的工具库来进行异步的CSS文件加载,它会在CSS文件加载完毕后执行回调。不过,异步加载CSS也会带来一个新的问题,如果我们将所有的CSS全部设置为了异步加载,那么用户会首先看到单纯的HTML页面,这也会给用户不好的体验。那么我们就需要在异步加载与首屏渲染之间找到一个平衡点,即首先加载那些必要的CSS文件。
我们一般将首屏渲染中必要的CSS文件成为Critical CSS,即关键的CSS文件,代指在保证页面的可读性的前提下需要加载的最少的CSS文件数目。Critical CSS的选定会是一个非常耗时的过程,特别是我们网站本身的CSS样式设置也在不停变更,我们不可能完全依赖于人工去提取出关键的CSS文件,这里推荐Critical这个辅助工具能够帮你自动提取压缩Critical CSS。下图的一个对比即是仅加载Critical CSS与加载全部CSS的区别:

4293272407-57bfe529b0ae0_articlex

上图中红色的线,即是所谓的折叠分割点。

服务端与缓存

高性能的前端离不开服务端的支持,在我们的实践中也发现不同的服务端配置同样会影响到前端的性能。目前我们主要使用Apache Web Server作为中间件,并且通过HTTPS来安全地传递内容。

Configuration

我们首先对于合适的服务端配置做了些调研,这里推荐是使用H5BP Boilerplate Apache Configuration作为配置模板,它是个不错的兼顾了性能与安全性的配置建议。同样地它也提供了面向其他服务端环境的配置。我们对于大部分的HTML、CSS以及JavaScript都开启了GZip压缩选项,并且对于大部分的资源都设置了缓存策略,详见下文的File Level Caching章节。

HTTPS

使用HTTPS可以保证站点的安全性,但是也会影响到你网站的性能表现,性能损耗主要发生在建立SSL握手协议的时候,这会导致很多的延迟,不过我们同样可以通过某些设置来进行优化。

  • 设置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

我们并没有使用某个服务端框架,而是直接使用了静态的Apache Web Server,不过Apache Web Server也是能够读取Cookie并且进行些简单的操作。譬如在下面这个例子中我们将CSS缓存信息存放在了Cookie中,然后交付Apache进行判断是否需要重复加载CSS文件:

这里Apache Server中的逻辑控制代码就是有点类似于注释形式的<!-- #,其主要包含以下步骤:

  • $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文件已经被加载过了,因此可以直接从本地缓存中加载而不需要重复请求。

上述策略同样可以应用于Web Fonts的加载,最终的Cookie如下所示:
3888680170-57bfe53b2a5e6_articlex

File Level Caching

在上文可以发现,我们严重依赖于浏览器缓存来处理用户重复访问时资源加载的问题,理想情况下我们肯定希望能够永久地缓存CSS、JS、Fonts以及图片文件,然后在某个文件发生变化的时候将缓存设置为失效。这里我们设置了以https://www.voorhoede.nl/assets/css/main.css?v=1.0.4形式,即在请求路径上加上版本号的方式进行缓存。不过这种方式的缺陷在于如果我们更换了资源文件的存放地址,那么所有的缓存也就自然失效了。这里我们使用了gulp-rev以及gulp-rev-replace来为文件添加Hash值,从而保证了仅当文件内容发生变化的时候文件请求路径才会发生改变,即将每个文件的缓存验证独立开来。

Result

上面我们介绍了很多的优化手段,这里我们以实验的形式来对优化的结果与效果进行分析。我们可以用类似于PageSpeed Insights或者WebPagetest来进行性能测试或者网络分析。我觉得最好的测试你站点渲染性能的方式就是在限流的情况下观察页面的呈现效果,Google Chrome内置了限流的功能:
4159997457-57bfe53fd55a2_articlex
这里我们将我们的网络环境设置为了50KB/S的GPRS网络环境,我们总共花费了2.27秒完成了首屏渲染。上图中黄线左侧的时间即指明了从HTML文件开始下载到下载完成所耗费的时间,该HTML文件中已经包含了关键的CSS代码,因此整个页面已经保证了基本的可用性与可交互型。而剩下的比较大的资源都会进行延时加载,这正是我们想要达到的目标。我们也可以使用PageSpeed来测试下网站的性能,可以看出我们得分很不错:
pagespeed-insights-voorhoede
而在WebPagetest中,我们看出了如下的结果:
426616130-57bfe551ba828_articlex

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服务来减少服务端与客户端之间的物理距离,从而减少传输时延。