如何运用最新的技术提升网页速度和性能

最近更新了我们的网站,它是经过了设计上的全面验收的。但实际上,作为软件开发者,我们会注重很多技术相关的零碎的东西。我们的目标是控制性能,注重性能,未来可伸展,为网站增添内容是一种乐趣。接着就来告诉你,为什么我们的网站速度比你们的快吧(抱歉,确实是这样的)。


性能设计


在我们的项目中,我们每天都会和设计师和产品负责人讨论关于平衡美观和性能的问题。对于我们自己的网站,这样做是很简单的。简言之,我们认为好的用户体验从快速的内容传输开始,也就意味着 性能 > 美观

好的内容、布局、图片和交互是吸引用户的重要因素。这每个因素都会影响页面的加载时间和终端用户体验。每一步我们都在探讨如何在获得好的用户体验和保证设计美感的同时,最小化对性能的影响。


内容优先


我们想要把核心内容尽快地呈现给用户,意味着我们要处理好基本的 HTML 和 CSS。每个页面都应该达到基本的目的:传递信息。JS、CSS、网页字体、图片、网站分析等优化都是位居于核心内容之下的。


可控性


给理想网站定义了标准后,我们总结出:要想达到预期效果,就要能对网站各方面的控制都游刃有余。我们选择构建自己的静态站点生成器,包括资源传输,并且由我们自己操控。

静态站点生成器


我们用 Node.js 实现了静态站点生成器。它是采用带有简短 JSON 页面描述标签的Markdown 文件来生成整个网站结构和它所有的资源。为了包括特殊的页面脚本,也可以附带一个 HTML 文件。以下是一个简单化的描述标签和 markdown 文件,用于博客的发布,用它来生成真正的 HTML。


JSON 描述标签:

{
 "keywords": ["performance", "critical rendering path", "static site", "..."],
 "publishDate": "2016-07-13",
 "authors": ["Declan"]
}

markdown 文件:

 # Why our website is faster than yours
 We've recently updated our site. Yes, it has a complete...

 ## Design for performance
 In our projects we have daily discussions...

图片传输


平均一个 2406kb 的网页中 1535kb 是图片。就因为图片在网站中占据了这么大的一个比例,所以它也是性能优化的重点之一。

WebP格式


WebP是一种现代图片格式,为网页图片提供了出色的低损耗、有损压缩。WebP格式的图片实质上比其它格式的小,有时可以比同样的 JPEG 图片小 25%。 WebP被大多数人所忽略,也没被经常使用。截止到写这篇文章的时候,WebP 仅支持Chrome, Opera 和 Android (仍超过了我们50%的用户),但我们可以优雅降级为 JPG/PNG。

使用<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">
  [站外图片上传中……(6)]
</picture>

我们使用Scott Jehl 的 picturefill 来使那些不支持<picture> 元素的浏览器获得支持,在各个浏览器中达到一致的效果

我们使用 <img>作为那些不支持<picture>或者 JS 的浏览器的后备元素。使用图片的最大实例确保了它在后备方案中的可行性。

生成


尽管图片传输方式已经确定了,我们仍需要思考该怎样有效地执行。我喜欢 <picture>元素的功能,但不喜欢写上面那些代码段,尤其是写内容时必须把它加进去。我们不想做这么费力的事情:每张图片都要写 6 个实例,所以优化这些图片并且把它们写在markdown文件的 <picture> 里面。所以:

生成图片


在构建过程中,原图片的多个实例,包括JPG, PNG和WebP格式,我们使用 gulp responsive 来生成。

最小化图片


在markdown文件中写[图片描述](image.jpg).

在构建过程中使用自定义Markdown渲染器来为已经完全成熟的 <picture>元素编译传统的markdown图片声明。

SVG动画


我们为自己的网站选择了特定的图标类型,其中SVG插图占了主要地位。这样做有以下几个原因:

  • 首先,SVG的图片比位图更小
  • 其二,SVG图片本身就是响应式的,有很棒的伸缩性, 所以不需要图片生成及<picture>元素;
  • 最后也是很重要的一点,就是我们可以通过CSS来不断改变它,赋予它新的活力!我们所有的组合页面都有一个自定义的动态SVG图, 可以被概述页面所复用。这张图片作为我们所有组合页面的一种循环风格,使得页面设计一体化,同时又几乎不会对性能造成影响。请看这张动画,看看我们是如何用CSS来改变它的。

[站外图片上传中……(7)]</img>


自定义网页字体


在深入之前,这里有一个关于在浏览器设置自定义字体的简短介绍。当浏览器发现CSS里面有@font-face 的定义,但是用户的电脑并不支持该字体时,它会尝试下载该字体文件。在下载时,多数浏览器根本不会用这种字体来展示文本。这种现象称为“不可见文本的闪现” 或者 FOIT。如果你有留意,你会发现网页上都有这种情况存在。如果你问我,我会告诉你这会影响用户体验。它延迟了用户读取他们所需内容的时间。我们可以迫使浏览器改变这种行为,变成 “无样式内容闪现” 或者称为 FOUT。

我们告诉浏览器先使用普通字体,像 Arial 或者 Georgia。当自定义的字体下载完成后,再代替标准字体并且重新渲染。这样,即使自定义字体下载失败,仍然不会影响内容的可读性。然而,有人会认为这是一种妥协的做法,但我们认为自定义字体只是一种优化。尽管没有自定义字体,网页看起来也完好,也能百分百的正常运行。勾选/不勾选复选框来切换我们的网页字体,来自己体验一下:

切换下载的字体类 使用自定义网页字体可以改善我们的用户体验,只要你能够优化他们,并且负责任地为之服务。

字型子集设定


到目前为止,子集设定是改善网页字体性能最快的方式。我将会向每个使用自定义字体的网页开发者推荐它。如果你能完全控制网页内容,并且知道它将要展示哪些特性的话,你可以完全使用子集设定。但是,即使是仅仅把字体设为西方语言,也会对文件大小造成很大的影响。例如,我们的 Noto Regular WOFF 字体,默认是246KB,将其设为西方语言后,大小下降到31KB。我们使用 Font squirrel webfont, 这种字体真的很易用。

字体监听器


Bram Stein 推出的字体监听器是一个很了不起的脚本,可以帮助检查字体是否已被加载。至于你是如何加载字体的,是通过一个网页字体服务,还是自己上传就不可知了。在这个监听器告诉我们所有自定义的字体已经下载完毕后,我们就可以在 <html>元素上添加一个字体加载完毕的类,我们的页面就会重新用新的字体:

  html {
     font-family: Georgia, serif;
  }

  html.fonts-loaded {
     font-family: Noto, Georgia, serif;
  }

注意: 为了简短,我没有给上面CSS中的 Noto 加上 @font-face 的声明。

我们可以设定一个cookie来记住所有的字体已经被加载过,就可以让他们缓存在浏览器里面了。我们使用这个cookie来做重复的浏览,这个我后续会解释。

在不久的将来,我们或许不需要 Bram Stein 的脚本来监听这个行为。CSS开发团队已经提案一个新的 @font-face 描述器,也叫 font-display。它的属性值控制着一个可下载的字体是如何在还没加载出来时就渲染页面的。这是CSS对font-display的描述:它将带给我们像上面方法一样的行为效果。你可以读读更多关于 font-display 的属性。


JS和CSS懒加载


一般来讲,我们都是尽可能快的加载需要的资源。我们移除一些堵塞的请求来加快页面渲染,优化首屏,用浏览器缓存来处理重复的页面。

JS懒加载


设计上,我们的网站并没有很多JS。我们发展了一个JavaScript工作流来处理我们目前已有的js, 以及未来会用到的js资源。


JS在 <head>块里面渲染,这是我们想要的。JS应该只是用来提高用户体验,不应该是访问者需要的关键。处理JS堵塞渲染的简单方法就是把脚本放在页面的尾部。这样网页就会在整个HTML 渲染完毕后才去加载JS。


另一种可以把脚本放在<head>执行的方案是在<script>标签里面添加defer属性,来延迟脚本的执行。由于浏览器下载脚本是很快的,不会堵塞页面渲染进程,等到页面完全加载完,才会执行脚本里面的代码。还有一件事,我们没有使用像jQuery这些库,所以我们的脚本取决于 vanilla 脚本的特性。我们只是想要在浏览器加载脚本来支持这些特性。最终的结果就像下面的代码这样:

 <script>
     if ('querySelector' in document && 'addEventListener' in window) {
         document.write('<script src="index.js" defer><\/script>');
     }
 </script>

我们把这小段脚本放在页面头部,来检测浏览器是否支持原生JavaScript的document.querySelectorwindow.addEventListener属性。如果支持,我们通过<script>标签直接给页面加载脚本,并使用defer属性让它不会堵塞页面渲染。


懒加载CSS


对于首屏来讲,网站最大的渲染堵塞资源就是CSS了。只有当<head>里面的CSS文件完全加载完毕时,浏览器才会开始渲染页面。这种做法是经过深思熟虑的,若不是这样,浏览器就需要在整个渲染过程中不断重新计算布局尺寸,不断重绘。

为了防止CSS堵塞渲染,我们就需要异步加载CSS文件。我们使用了 Filament Group的 awesome loadCSS 函数。该函数提供了一个回调,当CSS文件加载完后,我们设置一个cookie来声明CSS文件已经加载了。我们使用这个cookie来重现页面,这一点我后续会解释到。

CSS异步加载也带来这样一个问题,尽管 HTML 真的很快被渲染出来,但看起来就只有纯粹的HTML,只有等到整个CSS文件加载完且停止时,才会有样式。这时就要提到关键CSS了。

关键CSS


关键CSS就是阻塞浏览器渲染出用户可识别的网页的一小部分CSS。我们注意网页的上半版版面。很明显,两个设备的版面的位置有很大的区别。因此,我们做了一个大胆的猜测。

手动地检测这部分关键性的CSS是个很耗时的过程,尤其是样式、特性等不断改变时。这里有几个好的脚本,可以在你构建网页时,生成关键性CSS。我们采用了 Addy Osmani的版本。

下面是我们分别用关键性CSS和整份CSS分别渲染的页面效果。注意到下半版仍然有部分内容还没有样式。

左侧网页是用关键CSS渲染的,而右侧网页则是用整份的CSS。红线是分界线。


服务端


我们自己部署 de Voorhoede 网站,因为我们希望能够控制服务器环境。我们也想要尝试,是否可以通过改变服务端的配置来大大提升性能。当前我们使用了 Apache 服务和 HTTPS 协议。

配置


为了提升性能和安全性,我们研究了如何配置服务端。

我们使用 H5BP apache样板配置,这个对于改善Apache网络服务的性能和安全性是个很好的开始。他们也有其他服务器环境的配置。对于我们的大部分 HTML、CSS 和 JS,我们使用GZIP压缩。对于我们的所有网站资源,都使用缓存HTTP标头的做法。有兴趣请阅读文件层级缓存的章节。

HTTPS


用HTTPS来服务你的网站会对性能造成影响。这主要是设置了SSL握手,引入了大量潜在的东西。但通常情况下,我们可以做一些改变!


HTTP Strict Transport Security 是一个HTTP标头,可以让服务器告诉浏览器只能用HTTPS来与它交互。这种方式防止HTTP请求被重定向为HTTPS。所有尝试用HTTP来访问站点的请求都会被自动转换成HTTPS。这样就节省了一个来回。


TLS false start 允许客户端在第一个TLS回合结束后,马上向后端发送加密的数据。这种优化为一个新的TLS连接减少了握手次数。一旦客户端知道了密钥,就可以开始传输应用数据。剩下的握手用来确认是否有人篡改了握手记录,并且可以并行处理。


TLS session resumption 通过确认浏览器和服务器是否已经取得联系来帮我们节省一个来回。浏览器会记住这一次的标识符,下次发起连接时,这个标识符就会被重用,节省了一个来回。


我听起来像是一个搞开发和运维的,但确实不是。我只是读过一些书,看过一些视频。我喜欢 Google I/O 2016 的 Mythbusting HTTPS:Emily Stark 的安全性都市传奇。

cookies的使用


我们没有用一门服务端的语言,只有静态的 Apache 网络服务。但一个 Apache 网络服务仍可以做包括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 -->

第一次浏览我们添加了<noscript>标签,里面放置了 <link rel="stylesheet">。之所以这样做,是因为我们要异步加载整份CSS和JS。如果JS不能用的话,这种做法是不能执行的。这意味着,我们要用常规的加载CSS的方法来做回退。

我们添加了一个行内的脚本来懒加载CSS,onloadCSS 回调里面可以设置cookies.

在同一份脚本里面,我们异步加载了整份CSS。

在 onloadCSS的回调里面,我们用版本号来设置cookie的值。

在这个脚本后面,我们添加了一行关键CSS的样式。这个会阻塞渲染,但时间是非常短的,而且可以避免页面展示出来只有纯粹的HTML而没有样式的情况。

声明(意味着 css-loaded 的cookie 已经存在)用户重复浏览。因为我们可以从某种程度上来假定,CSS文件之前已经被加载过了,我们可以利用浏览器缓存来提供样式表。这样从缓存里面加载速度就很快了。同样的方法也被用来在第一次异步加载字体,后续的重复浏览也是从缓存里面获取字体。


这就是我们第一次和重复浏览时,我们用来区分的cookies。


文件级缓存


由于我们在重现页面时很大程度上依赖于浏览器缓存,所以我们需要确认我们的缓存是否合理。理想中我们是要永远的存储资源(CSS、js、 字体、图片),只有当这些文件被修改时才需要更新。当请求的URL是唯一时,缓存就会失效。每更新一个版本,我们都会用git tag 打个标签。所以最简单的方式就是给我们请求的URL加上一个参数(代码版本号),如 https://www.voorhoede.nl/assets/css/main-8af99277a6.css?v=1.0.4.。

但是,这种做法的缺点就是当我们要写一个新的博客post(这也是我们代码库的一部分,并没有永久地存储在CMS),原来缓存的资源将会失效,尽管没有改变原来那些资源。

就在我们尝试去改善这种方法时,我们发现了 gulp-rev 和 gulp-rev-replace 。这些脚本会自动合理地在我们的文件名称后面添加一窜hash值。这意味着只有实际上文件被改变时,才会去改变请求的URL,这样每个文件的缓存就会自动失效。这种做法让我兴奋不已啊!


结果


如果你看到这里,你应该是想要知道结果的。测试网页的性能可以采用像 PageSpeed Insights 这样的工具,它有很实用的提示。也可以使用 WebPagetest来测试,有扩展性的网络分析。我认为测试网页渲染性能的最好方法就是在疯狂地遏制网络通信时来观察网页的进程。这意味着,用一种不切实际的方法来遏制通信。在谷歌浏览器,你可以这样操作 (via the inspector > Network tab) 来限制通信,观察网页形成过程中,请求是如何缓慢加载的。

下面是我们的网页在 50KB/s 的情况下的加载状况。

这是 de Voorhoede site 首屏的网络分析,是网页第一次进程的一个概览。

注意到在50KB/s的网速中,我们是如何让首屏的渲染只用了 2.27 秒的。也就是第一张幻灯片和瀑布图里面的黄色线所代表的位置。黄线恰好绘在就是HTML已经加载完的时间位置。HTML包含了关键CSS,保证页面的可观性。所有其他的CSS都是用懒加载,所以我们可以等到全部资源加载完时才与页面进行交互。这也是我们想要的效果!

另一个值得注意的就是自定义字体从来不在这缓慢的链接上加载。 font face 观察器会自动注意到这一点。但是,如果我们不异步加载字体,你注视大多数浏览器,都会出现FOIT(不可见文本的闪现,上文有提及)的情况。

所有的CSS文件仅在8s后就都加载完毕。相反,如果我们不采用加载关键CSS的方式,而是采用加载全部CSS,我们在前8秒看到的将会是空白的页面。

如果你感到好奇,想与那些不太注重性能的网站对比一下加载时间,那赶紧试试吧。那个时间肯定是飞涨啊!

用上面介绍的工具测试了我们的网站,结果也是让人满意的。 PageSpeed insights 在移动性能方面给了我们100分,多么了不起啊!

PageSpeed insights 对 voorhoede.nl的测试结果! 速度100分!

当我们查看 WebPagetest 时,我们得到下面这样的结果:

可以看出,我们的服务器运行良好,首屏的速度指标是693。 这意味着我们的页面在693毫秒后就可以在宽屏缆线下被使用了。


技术路线


我们这样还不算完成,还会不断地重复我们的方法。我不久的将来,我们会主要关注以下内容:

HTTP/2


目前我们正在试验HTTP/2。本文所描述的大多数东西都是基于 HTTP/1.1 权限内的最好实践。简言之,HTTP/1.1 要追述到1999年,那时 table布局和行内样式都如火如荼。HTTP/1.1 从没为 2.6MB的网页要接受200个请求而设计。为了减轻旧版协议带来的痛楚,我们结合JS、CSS、关键性CSS,还为小图片设置数据源URI等。用各种方法来节省请求。自从 HTTP/2 可以在同一个TCP链接上平行地运行多个请求后,所有的这些联结使用和减少请求的做法都可能成为反面模式了。当我们跑完这个实验后,我们将会采用 HTTP/2 协议。

Service Workers


这是一个在后台运行的现代浏览器的 JavaScript API。它拥有很多特性,这些特性在以前的网站上都是没有的,如离线支持、消息推送、背景同步等。我们现在正尝试用 Service Worker, 但还是得在我们自己的网站上实现先。我向你保证,我们会做到的!

CDN


因此,我们想要自己控制和部署我们的网站。而且现在我们也要采用CDN来摆脱由服务端和客户端实际距离所带来的网络问题。尽管我们的用户基本上都是荷兰的,我们也想向世界的前端社区反映我们在质量、性能和推动网络发展方面做的最好。


相关文章


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 作者:Declan译者:旭日云中竹链接:http://www.zcfy.cc/article/1030原文:htt...
    51CTO学院阅读 385评论 1 1
  • 前言本文是@旭日云中竹带来的翻译与投稿。文章中提到的图片格式webp,自定义网页字体,懒加载等技术的运用,这些在大...
    Simon王小白阅读 274评论 0 1
  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,737评论 1 92
  • 一首温柔的诗 一首深情的诗 用我燃烧的热情 用我炽热的言语 你来过我生命里 悄悄地你又离去 一切仿佛不留痕迹 却深...
    _文生_阅读 173评论 5 2
  • 当自己最亲的人整天在你的身边耳提面命的告诉你:这个家现在所受的苦难都是因为你!当周围的人都用嘲笑的眼神告诉你:你太...
    阿小猫阅读 170评论 0 0