来源页面:http://copri.me/post/front-end-perf-translation.html
本文翻译自Css Wizardry上的Front-end performance for web designers and front-end developers。
对于任何一个还算过得去的网站,性能毫无疑问是要注意的一个非常重要的方面,无论这是一个个人作品站,或者是一个偏向手机的web app,亦或是一个巨大的电子商务站点。各种各样的研究、文章和个人经历告诉我们“越快越好”。
性能问题不仅仅是非常重要,也十分有趣,是我在工作中(我一直都缠着我们的首席性能工程师)、一些个人的项目里以及在CSS Wizardry(我一直缠着Andy Davies)中都要接触的东西。
在这篇老长的文章里会分享一大堆快速、简单明了而又耐人寻味的性能相关知识作为web设计师和前端开发者的入门指导。希望对于那些想要开始学习性能相关知识、并且想要提升他们的前端页面速度的人来说,这篇文章能够作为一篇比较合适的介绍。你可以比较轻松的运用下面讲到的所有技巧。仅仅需要一些小聪明和了解基本的浏览器工作原理,你就能开始加入整个这场“性能的游戏”了!
这篇长文里面没有大段大段让人疑惑的图表或者抓狂的数字,里面展现的是我通过不断阅读、运行监视、协作和玩弄(俺花了很多时间黏在CSS Wizardry的性能瀑布图上面)后积累下来的一手的性能相关的技巧。我也会在文章里链接一些类似主题的文章去进一步说明一些比较重要的部分呢。让我们开始吧!
注意:这篇文章确实需要一些基础的性能知识作为铺垫,不过如果在阅读的过程中有什么不太清楚的地方就去问Google吧!
基础部分
有些性能相关的事情几乎所有的设计师和前端工程师都会知道,比如尽量减少请求的数量,优化图片,把样式表放在<head>
里,把JS放在</body>
之前,压缩处理JS和CSS文件等。这些基础的东西已经给用户带来了更快的浏览体验,不过这些只是沧海一粟罢了。
需要注意的很重要的一点是:那些每天虐我们千百遍的浏览器是很聪明的。它们会暗自帮你做一大堆优化工作,所以很多的有关性能优化工作需要去了解浏览器会在哪些地方做文章,并且如何最大化利用这一点。不少有关性能调优技巧仅仅去从理解、利用和操控那些浏览器已经给我们做的东西入手。
样式放顶部,脚本放底部
这是非常基础而又很容易被应用的规则,但是为什么它有作用呢?简要的说是这样的:
- CSS会阻塞浏览器的渲染,所以你需要马上去处理它(即:放在文档头部
<head>
里) - JS会阻塞下载,所以为了防止它们耽搁页面上其它东西的显示你需要将其放到最后的地方。
CSS阻塞页面渲染是因为浏览器们会更倾向逐步地显示页面:它们一拿到样式,就开始迫不及待地将这些样式按顺序渲染出来。如果样式在页面下方的话,浏览器就会等那么一段时间才能考虑到样式的事情。这样浏览器在之前文档里改变了一些已经渲染的东西时可以防止样式的重绘。所以浏览器在获取到所有的样式信息之前不会去渲染页面,如果将这些信息放在文档的末尾的话就会阻塞浏览器的渲染。
所以将CSS文件放在页面顶部会让浏览器立马开始页面的渲染。
Javascript阻塞浏览器的下载有不少原因(浏览器显示聪明材智的另一个地方),不过首先我们需要了解浏览器是任何处理文件的下载的。简单的说,浏览器会从一个域名下并行下载尽可能多的文件。越多域的话,就有越多的文件能够被立即并行下载。
不过Javascript打断了这个过程,阻塞了从各个域下的并行下载,这是因为:
- 被调用的脚本可能会改变页面,这样浏览器在处理下一步之前将不得不面对这个问题。为了解决这个问题浏览器会停止所有的下载并专心放在脚本上。
- 脚本的载入通常是有顺序要求的,比如在载入一个jQuery插件之前需要先加载jQuery文件。浏览器会阻塞并行的下载,这样就不会出现同时下载jQuery和插件的情况。很明显如果同时下载jQuery和插件文件的话,插件文件一般会先下载完成。
因此,正是因为浏览器在处理JS的时候会停止所有其它文件的下载,把你的Javascript放到文档末尾通常是个好的选择。你肯定看到过一些网站在花大把时间加载第三方JS文件的时候阻塞了其它文件的获取从而造成一些区域长时间显示空白的情形,这就是Javascript阻塞的真实故事。
不过浏览器还算是比较聪明的。我将会引用一段Andy Davies给我的邮件里的内容,因为这里他讲的比我更好:
现代浏览器将会并行地下载JS,不过渲染只有在JS脚本执行结束之后才会开始(当然也需要被下载完毕)。
脚本的下载通常由浏览器预加载器完成。
当浏览器的渲染因为等待CSS或执行JS等情况而阻塞,它的预分析器将会扫描页面剩下的部分找寻能够下载的资源。
有些浏览器,比如Chrome,下载文件的时候不同的文件会有不同的优先级。举个栗子:如果脚本和图片都在同时等待下载的话浏览器会先下载脚本
聪明!
因此,要是一个页面尽可能快地渲染,将样式放在顶部。要防止JS阻塞页面的渲染,把脚本放在底部吧。
更少的请求数目
另外一个非常明显和基本的优化性能的方法就是下载更少的东西。每个页面的一个文件是一个额外的HTTP请求。为了渲染页面,浏览器将不得不去获取页面所需的每一个文件。每一个请求都可能会引发DNS的查找、重定向、404等等。每一个发出去的HTTP请求,不论是样式表、图片、Web字体、JS文件还是你能想到的其它文件,都会是潜在的巨大开销。将请求的数目降到最小是一个最容易的优化方法。
回到浏览器和并行。大多数的浏览器会一次只从每个引用的域下载一些资源,而JS会阻塞这些资源的下载。每一个HTTP请求都应该好好考量,不要轻视。
并行的最大化
为了让浏览器并行地下载更多的资源,你可以通过不同的域来提供。比如,一个浏览器只能同时从一个域获取两个文件,那么由两个域提供的话浏览器就能同时获取4个资源,3个域就是6个资源。
很多网站会有静态资源域名。你可以看到Twitter使用si0.twimg.com
来提供静态资源:
1 | <link rel="stylesheet" href="https://si0.twimg.com/a/1358386289/t1/css/t1_core.bundle.css" type="text/css" media="screen"> |
Facebook使用fbstatic-a.akamaihd.net
:
1 | <link rel="stylesheet" href="https://fbstatic-a.akamaihd.net/rsrc.php/v2/yi/r/76f893pcD3j.css"> |
通过使用这些静态文件、资源的域,Twitter和Facebook能够并行提供更多的文件。twitter.com
的文件和si0.twimg.com
的文件能够连续地下载。这是个增加页面下载并发数目的简单方法,如果联合CDN技术通过将资源放在合适的物理地点用来减少延迟的话就更好了。
上面讲的东西都挺好的,不过在后面的讨论中,我们将发现在某些特定的情况下,由子域名提供资源会对性能造成不好的影响。
到此为止,我们有了以下的关于性能的基础知识:
- 将样式表放在文档的顶部
- 将Javascript放在靠底部(可以放的地方)
- 尽量的减少请求的数目
- 由不同的域来提供资源可以使浏览器同时下载资源的数目最大化
HTTP请求和DNS查找
每当你从任意的域请求一个资源的时候,一个有相关头部信息的HTTP请求便发出,资源被找到,获得响应。这是一个简化了很多的过程,但是整个过程差不多就是这样。对于这样的一个HTTP请求,所有相关的资源都在这个往返的过程中涉及到。这些请求正是前端性能的主要瓶颈所在,因为正如我们说过的,浏览器被并发的请求数量所限制。这就是为什么我们会常用子域名,进而使这些请求发生在不同的域上,增大请求并发的数量。
不过这样做有会有一个问题,就是DNS查询。每一次(没有被缓存的)一个新的域被引用,相应的HTTP请求会受累于一个耗时不短的DNS查询(差不多20到120ms的一个时间)来确定资源真实的所在地。Internet由一个个IP地址连接,而可以由域名来引用,而DNS则管理这样一个对应的系统。
若每个被引用的新的域都在DNS查找上先花费时间的话,你最好确认这个花费是值得的。如果是一个小站(比如CSS Wizardry),那么由子域提供资源则有点不值得了。浏览器也许在从一个域下并发获取资源的耗时会比在多的域下执行DNS查找后并发获取的时间要短。
如果你有一大堆资源需要获取,那么你也许可以考虑将这些资源放在多个子域下,多出来的DNS查询时间对于更多的并发下载数目来说可能是值得的。如果你有比如40个资源,将它们放在两个子域下或许是很值得的,两次额外的DNS查询总共三个域给网站提供服务还是比较划算的。
DNS查询的代价很高,因此你需要确定上面的那种方案更适合你的网站:花些时间在DNS的查找上还是把所有的东西放在一个域下。
很重要的一点是一旦HTML从比如foo.com
获取到后,对这个域的DNS查找已经发生过了,所以接下来所有到foo.com的请求不会再受制于DNS查询了。
DNS预获取
如果你像我这样在网站上会放一些比如Twitter的控件、Analytics,亦或是Web字体,那么你将不得不链到这样的外部网站,而这时会引发DNS查找的。我的建议是一般在没有仔细考虑这些东西对性能的影响之前不要轻易使用它们,但是如果你是真的需要用到的话,下面的技巧会有些作用……
由于这些东西在其它的域上,这就意味着,比如你的Web字体相关的CSS将会和你自己网站上的CSS同时被下载,当然这是一个好的方面,但是脚本任然会阻塞(除非脚本的加载是异步的)。
这里真正的问题是DNS查找会牵扯到第三方的域名。不过幸运的是,有一种超快并、简单的方法来加速这个过程:DNS预获取。
DNS预获取就跟上面所描述的那样,能够被非常简单的实现。如果你需要从,比如widget.foo.com
获取资源的话,你可以通过简单的在<head>
里加入下面的片段来预先获取提供者的DNS:
1 | <head> |
这行简单的单面会告诉那些支持DNS预获取的浏览器在需要资源前先去查找相关域名的DNS。这意味着当浏览器处理到相应插件的<script>
元素的时候DNS查询已经完成了。它给了浏览器一个小起步。
这个简单的link元素(我用在了CSS Wizardry上)是一个完全向后兼容的,并且不会带来任何的性能影响。想想它能够带来多大的性能提升吧!
深入阅读
资源预获取
和DNS预获取一样,对网站的任何资源的预获取也很简单。为了明确那些资源是我们需要预获取的,我们必须首先了解浏览器是怎样在核实请求一个资源的。
CSS里面引用的Web字体和图片的表现大体上有相同。浏览器在处理到一段需要这些资源的HTML的文件时才会去开始获取它们。就如我之前所说的,这是浏览器非常聪明的一个例子。想象一下如果浏览器处理那些引用图片CSS的时候在声明的地方就开始下载这些图片:
1 | .page--home { background-image:url(home.jpg); } |
如果浏览器不等等真正需要这些图片的HTML出现的话,那么只访问主页则会将所有的图片都加载,这样真心很浪费啊,所以浏览器会确定页面是否是真的需要这些图片然后才会去下载它们。这儿的问题是对这些图片的下载不能马上进行,要到老远的地方才开始。
如果我们能够完全确定某张CSS里的图片在每个页面都会用的到,那么我们可以用个技巧让浏览器提前在用到它的HTML片段出现之前下载这张图片。达到这一点异常简单,如果你要求严格的话代码看起来也许有些糙。
不怎么漂亮的方法,看起来像是一个万能方法,就是在每个页面都放一个隐藏的<div>
,里面放有带有那张CSS图片src、alt属性置空的<img>
元素。我对CSS Wizarrdy的sprite图片就是这样处理的,这点上我很自信因为我确定所有的页面都会用到这张图。浏览器处理内联(inline)的元素的处理方式正如我们需要的那样会预获取,早早地下载到本地。所以让浏览器以<img>
的形式载入我的spirte图片会在真的需要它之前就加载完成。我就能够在HTML引用到图片的时候直接使用了。
第二种更“漂亮”一些的方法会有些比较困惑的地方,看起来和DNS预获取的例子比较像:
1 | <link rel="prefetch" href="sprite.png"> |
这段话明确的告诉了浏览器开始预获取我的sprite图片,不管它在分析了我的CSS文件后会做出的任何决定。
困惑主要是源自于两篇文章里面描述的不一致带来的。在一篇MDN的文章里,看起来这种预获取知识给了浏览器在闲置状况下一个可以开始预获取资源的提示。不过相反的是,在来自于Planet Performance的文章里指出浏览器如果支持rel="prefetch"
的话,就会预获取文件,没有谈到浏览器闲置状态的事情。我在DevTools的网络瀑布图里面看到的好像更支持后一种说法,不过WebKit很怪异的一点是在打开DevTools时候你不能动态观察预获取的情况,这样我不能100%的肯定。如果谁能把这一点说清楚那就感激不尽了。
我提过字体和图片的表现是差不多的,上面的那条规则对字体文件同样使用,不过你不能通过隐藏的div载入一个字体(需要通过预获取的link)。
1 | <link rel="prefetch" href="webfont.woff"> |
所以基本上我们这里做的就是对浏览器耍些花招让它提前下载资源,到应用我们的CSS的时候,就可以直接使用已经下载好的资源了(或者正在下载中)。漂亮!
深入阅读
CSS与性能
很多的建议都有说道如果使用资源域,你应该将所有的静态资源都放在上面。包括了CSS,JS,图片等。
不过我们在工作中发现,你不应当由资源子域提供CSS文件……
还记得之前我们讨论过CSS是怎样阻塞页面渲染的?浏览器希望尽快的获得你的CSS文件,越快越好。CSS是条关键路径。所谓关键路径是从用户发起请求到真正的在浏览器上看到什么东西的一条必经之路。正是由于它阻塞了渲染,CSS才是关键路径,JS和图片则不是。你所希望的是使这条路走的尽量快,所以最好没有DNS的查询。
在工作中,我们创建了这样一个网站,在接近线上的环境里所有资源全放在一个主机(比如foo.com
)下,不过当这个环境变得更像真实的场景的时候,我们有奖所有的资源放到了s1.foo.com
和s2.foo.com
下。也就是说所有的图片、JS、CSS、字体等都从这两个域下获取,这样引发了DNS的查询。问题在于,在查询缓存失败情况下,为了开始获取CSS文件而必须经历的DNS查询会拖慢这条关键路径的节奏。我们的图片大片的不清楚,说明出现了理应不存来的时延。最佳实践告诉我们应该将这些资源全放在子域上,不是么?不过不包括CSS。DNS查询耗费了大段的时间从而拖慢了整个网页的渲染。
正如Stoyan Stefanov指出的那样,由于CSS阻塞了页面渲染,所以CSS成为了性能面临的最大的敌人之一。同时值得注意的是浏览器在开始渲染之前会下载所有的CSS文件。这意味着浏览器即使只是将页面显示在屏幕上也会去获取print.css文件。这样即使在media query中用到的样式表 (比如<link rel="stylesheet" media="screen and (min-device-width: 800px)" href="desktop.css">
)就算不会被用到还是会被下载下来。
即便如此,Andy Davices曾经告诉我说WebKit实际上会给调整CSS文件的优先级,这样那些在初始化渲染时所需要的CSS文件就会首先被下载,而其它的,比如print.css
被尽量延迟到后面。太棒了!
在了解CSS这些情况后,根据CSS阻塞渲染、CSS文件全部被请求以及CSS的加载是一条关键路径,我们能够得出下面的一些结论:
- 永远不要将CSS文件放在提供静态资源的子域上,因为它会触发DNS查询造成渲染的延迟
- 尽早提供,这样浏览器可以继续做事
- 合并它们,反正浏览器会加载所有的CSS文件,把它们合到一起减少HTTP请求何乐不为呢
- 压缩和最小化,浏览器下载文件变小
- 缓存它们!减少上面几点的发生
CSS是一条关键路径,所以你应当尽早解决它,它阻塞了渲染,意味着降低了用户体验。将CSS移到子域上会降低网站的性能。
###深入阅读
压缩和最小化
这是两个你能(并且应该)对文字类资源做的事情。去除多余的空格和注释来最小化,通过压缩进一步缩减文件的大小。
如果两者选其一的话,仅压缩会比最小化更高效。不过,你应该尽量两个都用。
将gzip压缩打开通常需要一些.htaccess的技巧,不过我的好朋友Nick Payne指出,.htaccess从服务器端来看并不是特别好的方式,这是因为每次有新的请求时.htaccess都会被计算一次,所以实际上会有很大的消耗。
下面摘自Apache的官方文档:
如果你有修改服务器端配置文件的权限,你应该完全避免使用.htaccess文件。使用.htaccess文件会减慢你的
Apache服务器。你能在.htaccess文件里声明的指令最好放在
Directory块
中,这样会有相同的效果,并获得更好的性能
如果你仅仅有修改.htaccess的权限那么我就不用多担心了。这个开销通常不用太过于担心。在.htaccess里打开gzip压缩很简单。最小化文件倒是不那么容易,除非你有一个具体处理的过程,或者使用CodeKit、预处理器这样一类的同时直接得到最小化后的文件。
有趣的是,我将inuit.css迁移到Sass上主要是为了很方便的得到一个最小化的版本。
最小化在大多数情况下只是简单的移去空格和注释。如果你在代码里加了一大堆注释的话,我建议你还是好好最小化一下你的文件。
Gzip,正如其它的压缩算法一样,将任何文本输入通过对重复的/可重复的字符串操作而达到压缩效果。很多有重复字符串的代码都能够很高效地通过gzip进行压缩。比如:CSS文件里面重复的backgrounnd-image
,HTML里面的<strong>
标签。
Gzip能够很明显地减小文件的大小,你应该毫无疑问地启用它。这里有个很好的.htaccess文件作为模板。
压缩你的内容会节省大量的资源。在写这篇文章的时候,inuit.css大约有77kb。压缩后打大小只有5.52kb。最小化和压缩后的给我们减少93%的体积。并且由于gzip在文本文件上效果不错,你甚至可以对SVG和字体文件得到不错的压缩结果!
图片优化
除了使用一些优化工具外,我对优化图片的艺术了解的并不是很多,不过在处理图片上包括后期处理是一个很有趣的主题。
Spriting
CSS sprite技术对于一个高性能的网站是必不可少的,通过一个HTTP请求加载一张大的图片而不要通过几个请求加载几张小图。然而问题在于不是所有的图片是能够被切分放入sprite中。你也许需要一个图标作为一个不定宽度元素的背景,不过你显然不能将器sprite化,因为sprite在非固定宽度的元素上不那么好用。也许你可以用空白来填充图标左右的空间,但这些被浪费的像素又会会造成其它的性能问题
为了解决这些不能sprite化的元素,我们需要被称为“spriting元素”的东西。它基本上是一个空元素,一般用<i>
,它的作用就是一个带有背景的空元素。
在我开发Sky Bet、YouTube这样用,Facebook这样用,Jonathan Snook在SMACSS里有一大段都是它们。
基本的做法是,如果你不能将一个宽度不固定元素sprite化,那么就插入一个宽高确定的空元素,这样你就可是使用sprite技术了,比如这样:
1 | <li> |
在这里,我们不能sprite化<li>
元素或者<a>
元素,因此,我们在其中插入一个将图标作为背景的空<i>
元素。这是我关于性能最喜欢的事情之一,将优化性能的聪明做法和传统的“坏”标记结合起来。真是有趣!
Retina下的图片
你不需要将所有的图片都适应Retina屏幕。一个2x的图片包含有标准图片4倍的像素。四 倍 像 素。当然这不是说在传输的时候也是四倍大小-感谢图片自身的编码格式-不过这意味着一旦图片被解压缩并由浏览器渲染出来,4倍相对于标准图片大小内存将被占用。
如果我们好好思考一下,retina图片多数(即使不总是)是作为手机上高保真的UI组件显示的。手机的内存通常会比其它终端要少。Retina这烧内存的东西居然主要是提供给那些内存不那么多的终端用的……在你决定是否要全盘使用retina图片一定要三思啊,或者你可以做些明智的妥协?
视网膜屏幕(Retina)是很好的东西,清晰的体验,不过如果要花5秒去下载换得这样的体验是不值得的。在大多数情况下,速度要胜于美学。
你可以聪明地给所有的终端提供1.5x大小的图片,以换取还算好的图片体现,不过我觉得最好的选择还是谨慎使用retina图片。
如果你的数据表示可以接受的话,你也许可以使用SVG或者图标字体(icon font)代替位图图片。我在CSS Wizardry上用SVG带来了不少好处:
- 分辨率无关
- 可最小化
- 可压缩
工作中Matt Allen给我们做了一套图标字体,能够能和sprite元素一起提供retine支持可缩放的图标。
渐进显示的JPG
性能的一个有趣的方面是从人的感知所体现的。不是数字反应出来的东西,而是感觉上一个网站的载入速度有多快。
当显示大的JPG图片的时候,你也许对那种一点一点往下显示的载入方式更熟悉。先是100像素,然后暂停,接着50像素,暂停,突然图片另外200像素又跳了出来,哗,接着整个图片终于显示完全了。
这是传统的基本类型的JPG的工作方式,停停顿顿的体验。切换到渐进显示的JPG后你可以让它们通过更流行的方式载入:首先显示的是整个图片,不过都是一些像素块,然后逐渐地变得清晰起来。这听起来比前一种方式要差,不过感觉起来载入得更快。用户会马上有东西可看,并且图片的质量逐渐提高。这种类型的JPG会比基本的要稍微大一些,不过使体验快了许多。
要启用渐进式的JPG你仅需要在Photoshop另存为web格式的时候勾选相应的复选框就行了。收工!
深入阅读
完全不用图片
比用CSS sprite、SVG和避免retina兼容更好的做法就是什么图片都不用。如果你能100%用图片还原设计,而纯CSS方法能够做到75%的还原度,那么纯CSS的解决方案会更好(当然代码不要超过100行)。避免图片就潜在地减少了HTTP请求,也有助于维护。尽量不用图片把。
总结
于是我们有了一些 (但不怎么多)可做的事情可以用来利用浏览器并使你的前端交互更快。了解一些浏览器如何工作的知识可以让我们进一步操控它并提高我们前端页面的速度。
如果你有任何需要补充或者不同意、需修正的地方,请加入论坛中这一分支的讨论。性能的世界对我来说依然相对较新,因此我期待不断学习他人,并有所进步。
我真心希望这篇文字能在某种程度上启发到你,也让你接触到你之前可能从未想过的一些新东西。我也希望这篇文章可以帮助你了解到性能的乐趣。
我要特别感谢Nick Payne和Andy Davies在我写这篇文章的过程中帮助弄清了一些事情。太感谢了!
深入阅读
如果你觉得这篇文章不错,并且想了解更多的话,我强烈建议读读下面的文章:
- Souders’ High Performance Websites and Even Faster Websites.
- Stoyan Stefanov‘s site.
- Ilya Grigorik’s site.
- Andy Davies.