本文的作者哈利来自英国,是一位22岁的设计师、开发人员,同时也是一名作家和讲师(见下图)。不错的一篇文章,推荐熟读!
英文原文:Front-end performance for web designers and front-end developers ,编译:oschina
在这篇长文章中,我将分享个人项目经验,一些关于快速简单且非常有趣的WEB性能知识的点点滴滴,以便使你的行为可以像一个初级的网页设计师和前端开发者;希望对任何想开始学习性能的人,有一定的启迪作用,并使你的前端项目变得超快。我相信这些技巧你能很快掌握,并轻松实现。因为都只需要一点小技巧,以及一些浏览器怎样工作的基础知识。
注意 本文需要预先知晓一些基础的性能知识,如果有任何你不熟悉的就Google搜索一下好了!
基础知识
关于性能,有一些知识在所有的设计师和前端开发者中广为传播。例如,尽可能少的请求,优化图片,把样式表(stylesheets)放在<head>, 把JS放在</body>之前, 最小化(minifying) JS 和 CSS 等等。这些基础知识已经被用来加快用户响应了,但还有更多更多需要学习。
虽然在我们每天的工作生活中,浏览器给我们制造麻烦,使我们头疼,但请记住,他们也是很聪明的; 它们为我们做了很多性能优化工作, 所以大量的性能调优知识不但要知道浏览器在哪里给我们做了优化,还要知道怎么更好的挖掘它们。大量性能调优诀窍只是理解,利用和操纵浏览器已经替我们做好的优化工作。
顶部的Styles, 底部的scripts
这真的是一条基本规则,每个人都能非常容易的在大多数时间遵守,但为什么它重要?简短的说:
- CSS 块渲染, 因此你需要立即处理它(即在文档的顶部,在你的<head>之中)。
- JS 块下载, 因此你需要最后处理它们,以确保它们没有耽误页面中任何其它东西。
CSS块渲染是因为浏览器总是试图渐进式的渲染页面;它们想在元素到达的时候顺序的渲染它。如果style在距离很远的页面下部,浏览器在获得它之前没有办法渲染那个CSS。因为这个原因,如果浏览器在渲染文档过程中,改变了之前渲染的东西,它们可以避免style的重绘。浏览器在它获得所有需要的style信息之前不会渲染页面,如果你将style放在文档底部,你就是在使浏览器等待,阻塞了渲染。
所以,只要你将CSS放在页面的顶部,那么浏览器就可以立刻开始渲染。
JavaScript块下载是由于好几个原因(这又是浏览器聪明之处),但首先我们需要知道浏览器里的资源下载是如何实际发生的;简单的说,浏览器会从一个单一的域名并行的尽可能多的下载资源。它从越多的域名下载,就能在一瞬间并行的获得更多的资源。
JavaScript中断了这个过程,阻塞了从任何一个域名的并行的下载,因为:
- 被调用的脚本可能改变页面,即浏览器在继续别的事情以前,将不得不处理它。因此为了处理那个不测事件,浏览器停止了任何其它东西的下载,以便集中精力关注于它。
- 脚本正常工作经常需要依照一定的顺序加载,例如,要在加载一个插件之前加载jQuery。浏览器阻止了JavaScript的并行下载,因此它不会同时下载jQuery和你的插件;很显然如果你同时并行下载二者,你的插件会在jQuery之前到达。
所以,由于浏览器在获取JavaScript的时候停止了所有其他下载,将你的JavaScript脚本放在文档中尽可能晚加载的地方是一个好主意。我相信你们都看到过页面中的空白片段,在那里第三方的JS脚本被花时间加载,并且它还阻止了页面其他资源的获取和渲染;这就是JavaScript的阻塞在作用了。
但是显然,现代浏览器还是变得聪明了。我将给你一个Andy Davies寄给我的电子邮件的摘录,因为他解释的比我清楚:
现代浏览器将并行下载JS,只有在脚本被执行的时候阻塞渲染(显然脚本必须也被下载了)。脚本下载常常被浏览器的预加载器所完成。当浏览器页面渲染被阻塞,即等待CSS,或JS被执行,预分析器将扫描页面剩余部分,寻找它能下载的资源。有些浏览器如 Chrome, 将分先后下载资源,例如,如果脚本与图片同时在等待下载,它将先下载脚本。
漂亮的内容!
所以,要使页面被尽可能快的渲染,将styles放在顶部。为了阻止JS的阻塞影响到渲染,将scripts放在底部。
更少的请求
另一个明显而基本的性能优化方法是少下载。页面需要的每一个资源就是一次额外的HTTP请求;浏览器不得不停下来去获取每一个用于渲染页面所需的资源。每一次HTTP请求都可能引发DNS查询,重定向,404,等等。每一次HTTP请求,无论为了样式表,图片,web字体,JS文件还是其它你能想到的,都可能是一次非常昂贵的操作。尽量减少这些请求是你可以做的最快的优化方法中的一种.
再谈到浏览器和并行;大多数浏览器一次只从每个引用的域下载一些资源,而JS会阻塞这些下载。所以,你做的每一个HTTP请求都应该仔细考虑,而不是随便随便做的。
尽可能并行
为了让浏览器能并行的下载更多资源,你可以由不同的域名提供服务。如果说,浏览器只能一次从一个域名获取两个资源,那么由两个域名提供服务意味着它可以一次性获取四个资源;三个域名意味着六个并行下载。
许多网站有静态/资源 域名;你可以发现, 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技术就会更好,CDN技术通过从一个更加合适的物理位置提供资源服务的方法来减少延迟。
这全部都很好,但后面我们将讨论在特定环境下,怎样从子域名提供服务却会实际上对性能有害。
因此,现在有了我们关于性能的基础知识:
- 将样式表放在文档的顶部
- 将JavaScript放在底部(可能的地方)
- 尽可能减少HTTP请求
- 从多个域名提供资源服务能增加浏览器并行下载的资源数量。
HTTP 请求与 DNS 查询
每当你从任何域名请求一个资源,会发出一个带有相关头部,被访问资源的 HTTP请求,并且会返回一个响应。这是对该过程的一个极端简化,但它基本就是事实上你需要知道的。这是一个HTTP请求,而且所有涉及的资源都从属于这个往返的旅行。当提到前端性能,这些请求正是主要的瓶颈所在,因为如我们谈到的,浏览器受限于有多少请求可以并行发生。这也是为什么我们经常要使用子域名;以便允许这些请求在数个域名上发生,允许同时发生多得多数量的请求。
然而关于这还有个问题,DNS查询。每次(从一个空缓存)一个新的域名被引用,HTTP请求会受制于一个耗时的DNS查询(某个介于20到120毫秒之间的值),在DNS查询中,发出的请求会查询资源实际存在的地点;互联网通过IP地址被绑定在一起,这些地址由DNS管理的主机名引用。
如果每个引用的新域名具有DNS查询的前端代价,你必须确保这个代价确实是值得的。如果是一个小网站(例如像CSS魔法),那么由子域名提供资源可能并不值得;相比执行多个域名的DNS查询并将其并行化来说,从一个域名非并行的获取若干资源,浏览器可能更快。
如果你或许有一打资源,你可能会考虑从一个子域名提供它们的资源服务;为了更好的并行化那许多资源,额外的DNS查询可能是值得的。如果说你有40个资源,可能将那些资源切分到两个子域名是值得的;为了由总数为三个的域名提供你的网站服务,两个额外的DNS查询会是值得的。
DNS查询代价很高,因此你需要决定什么才是对你的网站更合适的;承担查询的消耗或者只是由一个域名提供所有服务。
很重要的需要记得的是,比方说一旦HTML被请求于foo.com,对那个主机的DNS查询就立即发生了,所以后续的任何对foo.com的请求不再受制于DNS查询。
DNS 预取
如果你像我一样想在网站上有一个Twitter小程序,还有网站分析,再也许一些网页字体,那么你必须要链接到一些其它域名,这意味着你将不得不引发DNS查询。我的建议通常是,不要还没有先适当的考虑性能影响就使用某个或任何一个小程序,但对于你认为确实需要的,下面的将很有用……
因为这些东西都存在于其它域名,比方说这就意味着你的网站字体CSS将会同你自己的CSS并行下载,从某种意义上说是一种好处,但是脚本将仍会阻塞(除非它们是异步的)
事实上,这里的问题是DNS查询牵涉到了第三方域名。幸运的是,有一个相当快又简单的办法来加速这个过程:DNS预取。
DNS预取所做的恰恰就是凭证领餐(on the tin),它不能被简单实现。比方说,如果你需要请求来自widget.foo.com的资源,那么你可以通过简单的在页面的<head>里先增加下面这个来预取那个主机的DNS:
1
2
3
4
5
|
< head >
...
< link rel = "dns-prefetch" href = "//widget.foo.com" >
... </ head > |
那行简单的内容将会告诉支持的浏览器去开始预取那个域名的DNS,这要稍稍早于它实际需要的时刻。它意味着DNS查询过程,在浏览器<script>元素真正请求小程序的时候就已经在进行中了。这仅仅给浏览器增加了一个很小的开头。
这种简单的链接元素(就是我在CSS魔法上用到的)完全后向兼容,而且不会忽略性能影响。将它看作是性能提升增强吧!
延伸阅读
资源预取
和DNS预取一样,也可以顺便对你的站点需要的其它资源进行预取。为了弄清楚我们想要预取哪些资源, 首先我们需要了解浏览器通常会在什么时候以什么方式对资源发出请求。
CSS中引用的Web字体和图片表现基本相同;浏览器在碰到需要它们的HTML时开始对它们进行下载。就和我在前面提到那样,浏览器非常聪明,这又是一个例证。想象一下,浏览器一看到下面的CSS声明就开始下载其中所引用的图片:
1
2
3
4
|
.page--home { background-image : url (home.jpg); } .page--about { background-image : url (about.jpg); } .page--portfolio { background-image : url (portfolio.jpg); } .page--contact { background-image : url (contact.jpg); } |
如果浏览器不是等碰到需要这些图片的HTML再下载它们,那么访问主页就会立即下载所有这四个图片。这会造成浪费,所以浏览器一定会确保在需要这些图片时才会开始下载它们。所以,这里有个问题在于,图片下载直到很晚才会开始。
如果我们可以完全确认某个CSS图片肯定会在每个页面都会用到的话,我们就可以用个小把戏让浏览器早早下载好这个图片,无需等到让浏览器碰到需要使用该图片的HTML才开始下载。想做到这一点也非常简单,但所用的方法可能会有点糙,就看你怎么弄了。
比较糙的方法和大多数笨拙的万全之法类似,就是在每个页面放置一个隐藏的<div>,在该div中使用带有空的alt属性的<img>标签。我在CSS Wizardry项目中的精灵中就是这么干的;因为我知道,每个页面都要使用该精灵,所以我就通过在HTML中对其进行引用对它进行预取。浏览器处理内联(inline)<img>的方式非常好,浏览器会早早地对它们进行预取,所以通过让浏览器将我的精灵作为HTML中的<img>进行载入,浏览器就可以在使用需要精灵的CSS之前将其下载好。通过首先在我的HTML中引用该精灵(隐藏起来的),我就能够抢先把精灵下载好。
还有第二种方法比较”优雅”,但会让人有些困惑。它和DNS预取的例子非常相似
1 | < link rel = "prefetch" href = "sprite.png" > |
这会显式地告诉浏览器,马上开始预取我的精灵图片,而不要考虑在它处理CSS时可能会做的任何决定。
令人感到困惑之处在于有两篇文章似乎有不同的观点;基于来自MDN的这篇文章,貌似这种预取指令只是示意浏览器仅在它空闲时才有可能会对href所指的资源进行预取。然而,与此矛盾的是,来自Planet Performance的这篇文章貌似在说,如果浏览器支持rel=”prefetch”的话,它就一定会预取href中所指的资源,并没有提及是否要在浏览器空闲时才进行预取。我在WebKit的Inpsector中的瀑布图中所看到的情况是后者说得是对的,但是在打开Developer Tools的情况下(薛定谔测不准。。。)WebKit的表现及其怪异,我就观察不到预取动作的情况了,这也就是我说,我无法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查询占据了大量的时间,进而延迟了页面的渲染。
因为有这种渲染阻塞阶段,CSS是性能最坏的敌人之一,正如Stoyan Stefanov阐述的那样 。而且也很有必要注意到浏览器在它开始渲染页面之前将下载所有的CSS。这意味着即使浏览器仅仅在屏幕上渲染页面,也要请求print.css。任何只是基于一种媒体查询的样式表(如<link rel=”stylesheet” media=”screen and (min-device- 800px)” href=”desktop.css”>)都将会被下载,即使并不需要它们。
即便如此,Andy Davies 告知我WebKit实际上提高了CSS下载的优先级,以便只有渲染页面需要的CSS先到达,而其他的样式,如print.css尽可能的延迟。漂亮!
知道这些关于CSS的信息已经允许我们做出一些决定,这些决定全部基于CSS阻塞渲染,要全部被请求,以及它位于关键路径的知识:
- 永远不要从一个固定/资源域名提供服务 因为这会引起DNS查询并进一步延迟渲染。
- 先提供服务 因此浏览器可以继续忙下去。
- 合并它 因为不管怎样浏览器会获取所有CSS,你最好将所有这些压缩于一个HTTP请求。
- 压缩并简化它 以便浏览器需要下载的少一些。
- 缓存它的一切 以便上述的过程尽可能少的发生。
CSS位于关键路径,因此你需要尽早先解决它,它阻塞渲染就意味着降低了用户的性能体验。 把CSS移到子域名会损害性能。
延伸阅读
压缩与简化
对于你的文本资源,有两个实在很简单的事情是你能(而且也应该)做的;简化他们移除任何注释和空格,并且进一步的压缩它们大小。
如果你想选择其一,单独的压缩要比单独的简化更有效。然而,如果可能的话你应该两个都做。
实施压缩经常需要一点.htaccess诡计,但如我的好朋友 Nick Payne指出的,.htaccess实际上从服务端的观点来看不是特别有性能;.htaccess评估每一个到达请求,因此实际它有很多开销。
这取自 Apache 文档 :
你应该完全避免使用.htaccess文件,如果你可以直接访问http主服务器的配置文件的话。 使用.htaccess文件使你的Apache http server慢下来。任何你能包含进一个.htaccess文件的指令最好设置在一个字典 块,因为它具有同样的效用并且有更好的性能。
如果你确实只是访问.htaccess,那么我不会担心;这个开销的代价通常无需关心。实际上通过.htaccess来压缩实现起来很简单。而简化不是那么容易,除非你有一个构建过程,或者用一些类似代码工具套件,或者能直接编译输出最小化的预处理器。
有趣的是,我移动inuit.css到Sass的主要原因最初是——我可以方便的编译一个简化的版本。
简化(Minification)最主要的部分是简单的删除空格与注释;如果你在代码中写的注释像我一样多,那么你确实需要缩减你的资源。
像任何压缩算法一样,压缩(Gzip)将任何基于文本的输入,基于重复的/可重复的字符串对其进行压缩。通过gzip大多数代码压缩得很好,因为所有代码都有包含重复字符串的倾向;例如CSS中一遍又一遍的background-image,标签中一遍又一遍的<strong>…
压缩真的大量的压榨掉资源的大小,你应该明确的启用它。为了能有规范的.htaccess片段,查阅 HTML5 样板处理资源 。
压缩内容引起大量的节约。在写操作的时候,inuit.css 输入有77k大小。压缩以后只有5.52k。简化与压缩给我们节省了93%。而且因为gzip对基于文本的资源工作的很好,你甚至可以压缩可缩放矢量图形(SVGs)和一些字体格式文件!
优化图像
相比通过优化工具运行而言,我对优化图像的艺术不是非常的知识广博,但通过图像自身,后加工来解决是一个相当有趣的话题。
Spriting (精灵)
如果想要一个性能优异的网站,Sprites(精灵,一种网页图片应用处理方式,它允许你将一个页面涉及到的所有零星图片都包含到一张大图中去)是几乎强制性的;在一个HTTP请求里加载一个大的图片,而不是若干个请求若干个图片。但是问题在于,不是所有的图片都可以立即精灵化;可能你有一个图标,需要将它作为一个弹性宽度元素的背景图像,但你显然不能将其精灵化,因为sprites对非固定尺寸元素不起作用。你通常只要在sprite表中的图像周围放许多空格,但这样在sprite中浪费像素它们自己就影响到性能了。
为了解决特定元素的不可精灵化,我们需要一种称为”精灵元素”的东西。这基本是一个空元素,一般是一个<i>,它的核心工作就是保持空并且加载一个背景图像。
在我创建Sky Bet时我用过这些,YouTube用它们, Facebook用它们, Jonathan Snook 有 一篇SMACSS的文章整个章节都关于他们。
基本的前提是,如果因为一个元素是流态的而不能精灵化,那么你在它里面放置一个空元素解决尺寸的问题,然后就可以精灵化了,例如:
1
2
3
4
5
|
< li >
< a href = "/profile/" >
< i ></ i > Profile
</ a > </ li > |
这里我们不能精灵化<li>或者<a>,所以我们在那里有一个空的<i>,它替代加载图标。这是关于性能我最喜欢的事情之一;你正将聪明的技术整合来改善页面速度,却仍然在使用传统的”坏”标记。有趣的定西!
延伸阅读
视网膜图像
你不需要将所有都提高到视网膜级别。在标准分辨率下,同样的图像放大2倍将包含四倍数量的像素。四倍啊。然而这并不意味着需要通过连接传输四倍的文件大小——感谢图片自身的编码格式——也就是说一旦图像解压并在浏览器中渲染,有四倍数量于平常的像素需要存储于内存。
如果停下来思考一下;视网膜图像最常(即使不是总是)需要用于给手机提供一个保真的UI。手机内存比其他设备少很多。视网膜效果给内存并不很多的设备提供了消耗内存的图像……反复全面的考虑一下你是真的需要视网膜图像,或者还是你可以做出一个明智的妥协?
视网膜效果是一种很棒的,清晰的体验,但如果需要5秒钟时间下载的话,将不会有清爽的体验。在大多数情形速度要胜过美感。
为了给每个人提供足够好的图片,你可以很聪明的给所有设备提供1.5倍的图像,但就我的观点——最好的选择是节俭的使用视网膜。
如果统计数据表明有足够富余,你就可以针对矢量图形优化,或者用字体图标代替位图。在CSS魔法网站我使用了矢量图形,这给我带来了如下好处:
- 分辨率无关
- 可简化
- 可压缩
在工作中 Matt Allen 给我们做了一种字体图标,可以同主要元素一起使用提供一种准视网膜的,可伸缩的图标。
你也可以看看怎样使用类似ReSRC.it的服务,以便基于设备与上下文加载图片。
渐进的 JPGs
性能的一个有趣的方面是感知性能;不是非要你的数字告诉你,而是一个站点感觉起来有多快。
当显示大的JPG图像时,可能你对它的一顿一顿的下载再熟悉不过;图像传输一百个像素,停顿,再五十个,停顿,突然一下子再两百个像素,整个图像加载完毕。
这是JPG图像的传统的工作基准,真的是一种非常卡的体验。通过切换到渐进的JPGs,你能使它们以一种更优异的流行方式加载;它们首次显示的是整个图像,但像素不是很清晰,然后慢慢的聚焦。这听起来比前面的方法要糟糕,但它感觉起来更快;用户立即就有东西可看,而且图像的质量逐渐的提高。典型的这些图像要比它们的基准副本大一点,但是却使整个体验感觉快了许多。
启用渐进的JPGs,你只要简单的在Photoshop中为web与设备保存图像时,检查一下相关的复选项;完工!
延伸阅读
完全不用图片
比spriting、可缩放矢量图形、以及避免视网膜效果更好的是避免所有图片。如果你能用图片100%复制一个设计,但是用纯CSS 75%复制,那么宁可用纯CSS解决方案(当然如果它不会最终超过100行的代码的话!)避免图片意味着避免了可能的HTTP请求,但有助于维护。如果你能避免使用图片那么试着那么做吧。
总结
因此我们有了一些 (但仍然只有一点) 事情可以去做,以此来挖掘浏览器潜力并使你的前端更快速。知道一些关于浏览器如何工作的知识,确实可以让我们进一步控制它并使前端更快。
如果你有任何补充——或真的不同意或想修正——那么请加入分论坛的讨论 ;性能的世界对我仍然相对较新,因此我期待从别人那里学习,并将一切进一步深入。
我真的希望这版文字能够至少某种程度启发到你,使你遇到你可能从未想过的一些新知识。我也希望,如果你没有准备好,这篇文章可以帮助你至少获得我发现的性能之中的乐趣的二分之一。
我要特别感谢Nick Payne 和 Andy Davies帮助澄清了一些写作本文时遇到的一些问题;谢了伙计们!