译者 | 京东金融-移动研发部-前端工程师 田腾
原作者 | Sanjay Purswani
来源 | Software Engineer at comparethemarket.com
优化事关速度和满意度
为了提升用户体验(User Experience,UX),我们希望前端提供快速加载和执行的网页。而对于提升开发者体验(Developer Experience, DX)来说,我们希望前端能够快速,简便和实用。
这样的优化不仅使我们的用户和开发者满意,也会显着提高SEO排名, 因为Google的SEO排名会偏向于优化较好的页面。如果你已花费了大量时间来提升自己网站的Google Pagespeed Insights分数,那么本文将有望揭示分数的实际意义以及为优化前端我们采取的大量策略。
背景
最近,我的整个团队有机会花费一些时间升级到我们的代码库,该升级可能使用React技术。这让我思考我们应该如何建立前端项目。很快我意识到浏览器将是我们考虑升级方法中的一个重要因素,同时也是我们知识的一大瓶颈。
方法
首先
我们不能控制浏览器或者改变它的行为方式,但是我们可以理解它的工作原理,用来优化我们页面的加载。
幸运的是,浏览器行为的基本原理相当稳定并有据可查,长时间内也不会显着改变。
这些特点至少给了我们一个努力的目标。
其次
另一方面,代码,堆栈,结构和模式是我们可以控制的。他们更灵活,改变地更快,为我们提供更多的选择。
因此
我决定彻底地弄清楚我们的代码最终结果应该是什么样,然后形成一个写这种代码的意见。 在这第一篇文章中,我们将专注于了解浏览器。
浏览器的功能
我们来建立一些知识。以下是我们期望浏览器运行的一些微不足道的HTML。
浏览器如何渲染页面
当浏览器收到我们的HTML时,会解析它,将其分解成一个自己理解的词汇,得益于HTML5 DOM规范,这些词汇在所有浏览器中保持一致。然后,浏览器将通过一系列步骤来构建和渲染页面。以下是高度概括的描述。
使用HTML创建文档对象模型(DOM)。
使用CSS创建CSS对象模型(CSSOM)。
执行DOM和CSSOM上的脚本(scripts)。
结合DOM和CSSOM形成渲染树(render tree)。
使用渲染树布局(layout)所有元素的大小和位置。
在所有像素中绘制(paint)。
第一步 – HTML
浏览器从上到下读取HTML的标记,将其分解成一个个节点来创建文档对象模型。
HTML优化策略
样式写在顶部,脚本写在底部
虽然这个规则使用起来也有例外和细微差异,但一般的想法是尽可能早地加载样式,尽可能晚地加载脚本。这样做的是因为脚本需要在HTML和CSS完成解析后再执行,因此我们将样式放到最前面,以便在编译和执行底部的脚本之前,有充足的时间来计算样式。 接下来我们进一步研究如何调整这个优化。
精简和压缩
这适用于我们提供的所有内容,包括HTML,CSS,JavaScript,图像和其他资源。精简删除任何冗余字符,包括空格,注释,额外的分号等。
压缩,例如GZip,将代码或其他资源中重复的数据,替换为指向原始实例的指针。大幅压缩下载文件的大小,而是依靠客户端解压缩文件。
通过这两项处理,你有望将有效载荷减少80%或90%。例如,将bootstrap减少87%。
可访问性
虽然这不会使你的网页下载速度更快,但会大大提高有身体缺陷用户的满意度。确保为所有人提供可用功能!在元素上使用aria标签,在图像上提供替代文本以及所有其他的不错功能(http://www.clarissapeterson.com/2012/11/html5-accessibility/)。使用像WAVE这样的工具来识别你可提高可访问性的地方。
第二步 - CSS
当浏览器找到任何与样式相关的节点,例如,外部,内部或内联样式时,它会停止渲染DOM并使用这些节点创建CSSOM。这就是为什么人们说CSS“阻塞渲染”。以下是不同类型样式利弊。
CSSOM节点的创建就像DOM节点一样,稍后它们将被组合起来,但现在它们是下面这个样子。
CSSOM的构造阻塞了页面的渲染,所以我们希望在渲染树中尽早地加载样式,使它们尽可能轻巧,并在有效的时候延迟加载。
CSS优化策略
使用媒体属性(https://developer.mozilla.org/en-US/docs/Web/CSS/@media)。
媒体属性指定加载样式需要满足的环境,例如,是否有最大或最小分辨率?是屏幕阅读器吗?
台式机非常强大,但移动设备不是,所以我们想要给他们最轻的有效载荷。我们可以假设一开始的时候只提供移动样式,然后将台式机上的样式放在媒体属性里,这样做不会阻止下载针对台式的机样式,但这些样式不会阻塞我们移动端页面的加载或耗尽移动端浏览器宝贵的资源。
延迟加载CSS
如果你有样式,可以等到第一个有意义的绘制之后再加载和计算,例如,在首屏看不到的内容,或者在页面响应之前不需要的内容。在添加样式之前,你可以利用脚本等到页面需要的时候再加载这部分样式。
这是如何实现的一个例子(https://jakearchibald.com/2016/link-in-body/),另一个例子(https://www.giftofspeed.com/defer-loading-css/)。
较少的选择器
将更多的元素链接在一起存在一个明显的缺点,即将会有更多的数据需要传输,从而扩大了CSS文件,另一方面,更多的选择器也会使客户端计算样式时消耗大量计算。
只提供你需要的
如果你作为前端工作了一段时间,就会知道CSS中的一个大问题是删除内容时的不可预测性。它设计时就被诅咒会不断增大。
要尽可能地精简CSS,请使用诸如uncss之类的工具,或者可以寻找类似的网站,有很多类似的选择。
第三步 - JavaScript
然后,浏览器会在找到任何JavaScript节点时继续构建DOM / CSSOM节点,即找到外部或内联脚本。
因为我们的脚本可能需要访问或操作前面的HTML或样式,所以我们必须等到二者全部构建完成。
因此浏览器必须停止解析节点,先完成CSSOM构建,执行脚本,然后继续解析。这就是为什么人们会说JavaScript“阻塞解析”。
浏览器有一个称为“预加载扫描器”的东西,它将扫描DOM脚本并开始预加载它们,但脚本只有在构建了先前的CSS节点之后才顺次执行。
如果这是我们的脚本:
那么这将对我们的DOM和CSSOM造成如下影响:
JavaScript优化策略
优化脚本是我们可做的最重要的事情之一,也是大多数网站做得最糟糕的事情之一。
异步加载脚本
通过在我们的脚本上使用一个异步属性(async),可以告诉浏览器先不管这个脚本,以较低的优先级在另一个线程下载它,不要阻塞页面其余部分加载。一旦完成下载,这个脚本将被执行。
这意味着该脚本可能在任意时间执行,这会导致两个明显的问题。首先,它可能在页面加载之后执行很久,所以如果我们依靠它为用户体验做一些事情,那么我们可能会给我们的用户一个不太好的体验。其次,如果它在页面加载完成之前执行,我们无法预测它是否能够访问正确的DOM / CSSOM元素并可能会中断执行。
async属性对于不影响我们的DOM或CSSOM的脚本是非常好的,对不需要我们的代码内容的外部脚本也是非常好,对于其他影响用户体验的脚本来说不是必需的,例如分析或跟踪。但是如果你发现任何合适的场景,请使用它。
延迟加载脚本
defer属性非常类似于async属性,它也不会阻止我们的页面的加载,但是,它会等到我们的HTML解析完成后再顺次执行。
这对于将对我们的渲染树上起作用的脚本来说是一个非常好的选择,但是对于加载首屏内容,或者先前的脚本运行完成后再执行的情况并不重要。
这是使用延迟策略的另一个非常好的选择(https://varvy.com/pagespeed/defer-loading-javascript.html),也可以使用类似addEventListener这样的东西。如果你想知道更多,那么这是一个开始阅读的好地方(http://stackoverflow.com/questions/588040/window-onload-vs-document-onload)。
不幸的是async属性和defer属性不适用于内联脚本,因为默认情况下,浏览器将会一旦遇到内联脚本就好编译并执行它们。当他们在HTML中内联时,它们立即运行,通过使用外部资源上的上述两个属性,相较于DOM / CSSOM我们可以延迟运行脚本。
操作前克隆节点
当且仅当您在对DOM进行多次更改时遇到意外行为时,尝试此操作。首先克隆整个DOM节点,对克隆的节点进行更改,然后更换原始节点可能会更有效,因为这会避免多次重绘降低CPU效率和内存负载。它还可以防止您的页面“抖动”和闪烁的未加载样式内容(FOUC)。
克隆时请小心,因为它不会克隆事件侦听器。有时候,这可能正是你想要的。在过去,当不调用命名函数,没有JQuery的.on()和.off()方法可用时,我们使用这种方法来重置事件侦听器。
预加载/预读取/预提交/预连接
这些属性基本上都是在他们的测试版本中,非常好用。但是它们是相当新的,并没有广泛的浏览器支持,这意味着我们大多数人并不是真的很重要的备选方法。但如果你有想了解,请看看这里(https://www.keycdn.com/blog/resource-hints/)和这里(https://css-tricks.com/prefetching-preloading-prebrowsing/)。
第四步 - 渲染树
一旦所有节点都被读取,并且DOM和CSSOM已准备好组合,浏览器就构建“渲染树”。如果我们将节点视为单词,将对象模型视为句子,则“渲染树”就是整个页面。现在浏览器具有渲染页面所需的一切。
第五步 - 布局
然后我们进入布局阶段,确定页面上所有元素的大小和位置。
第六步 – 绘制
最后我们进入绘制阶段,我们实际上光栅化了屏幕上的像素,为我们的用户绘制页面。
所有这些通常发生在几秒或零点几秒内。我们的工作就是做得更快。
如果JavaScript事件更改页面的任何部分,就会重新渲染“渲染树”,并强制我们再次布局和绘制。现代浏览器足够聪明,只会进行部分重绘,但是我们不能依赖此功能来保障我们的页面高效。
尽管如此,JavaScript在客户端主要是基于事件的,我们希望它处理我们的DOM,它仍将做这些处理。我们只需要限制它的不良影响。
现在你应该充分了解领会 Tali Garsiel的这个演讲。这是2012年的演讲,但信息仍然适用。她关于这个问题的全面论文可以在这里(https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/)读到。
如果你喜欢迄今为止所读过的内容,并仍然渴望了解更多底层技术知识,那么你所有知识的指导就是HTML5规范(https://www.w3.org/TR/html5/)。
我们快要完成整个介绍,只需再多给我一点点时间!现在我们揭示为什么我们需要知道上以上内容。
浏览器如何进行网络调用
在本节中,我们将了解如何最有效地向浏览器传送渲染页面所需的数据。
当浏览器向URL发出请求时,我们的服务器使用一些HTML进行响应。我们将从细微处开始介绍,慢慢增加复杂性。
假设这是我们页面的HTML。
我们需要学习一个新的术语,关键渲染路径(Critical Rendering Path,CRP)。它是指浏览器渲染页面所需的步骤数。这是我们的CRP图解现在看起来的样子。
浏览器发出GET请求,然后一直处于空闲状态,直到我们回复我们的页面所需的1kb HTML(还不包括CSS或JavaScript),然后可以构建DOM并渲染页面。
关键路径长度
三个CRP指标中的第一个是路径长度。我们希望这个值尽可能低。
浏览器需要一次往返请求服务器来检索渲染页面需要的HTML,它只需请求这一次。因此,我们的关键路径长度是1,完美。
现在我们继续下一个等级,在HTML里包含一些内部样式和JavaScript。
如果检查我们的CRP图表,我们会看到几个变化。
我们有两个额外的步骤,构建CSSOM和执行脚本。这是因为我们的HTML包含需要计算的内部样式和脚本。然而,由于不需要外部请求,因此它们不会增加关键路径的长度,耶! 但等一下,不要高兴得太早。还要注意,我们的HTML大小增加到2kb,所以我们必须在某个地方采取措施。
关键字节
三个指标中的第二个是关键字节。它测量渲染页面所需要传输的字节数。不是所有字节都需要下载到页面上,只有实际渲染页面中需要,及响应用户的内容才需要下载。
不用说,我们同样想尽量降低这个指标。
如果你认为这很好,不需要使用外部资源,那么你是错的。虽然这看起来很诱人,但它在规模上是不可行的。实际,如果我的团队把提供页面所需要的内容都放在内部或者行间,这个文件会变得非常大。并且浏览器的构造也不能够处理这么大的负载。
看看这个关于内联所有React recommends样式对页面加载的影响的有趣文章(https://www.ctheu.com/2015/08/17/react-inline-styles-vs-css-stupid-benchmark/)。 DOM变成四倍大小,需要两倍的时间才能安装,响应时间增长50%。这当然不能接受。
还要考虑外部资源可以缓存的事实,因此在再次访问页面,或访问其他使用相同资源(例如my-global.css)的页面时,浏览器将不会进行网络调用,而是使用缓存资源,这为我们赢得更大的胜利。
所以让我们更进一步,将样式和脚本引用为外部资源。请注意,我们有1个外部CSS文件,1个外部JavaScript文件和1个外部异步JavaScript文件
这是我们的CRP图解现在是这样。
浏览器获取页面,构建DOM,一旦碰到外部资源,预加载扫描器启动。它继续扫描并开始下载在HTML可以找到的所有外部资源。 CSS和JavaScript高优先级,其他资源优先级低一些。
它遇到我们的styles.css和app.js,就开辟出另外一个关键路径以获取它们。它没有加载analytics.js,因为我们给了它async属性。浏览器会在另一个线程以较低的优先级下载它,但是因为它不会阻止我们的页面渲染,因此它不会影响关键路径。这正是Google自己的网站优化排名算法。
关键文件
CRP指标的最后一项是关键文件。浏览器渲染页面必须下载的文件总数。在我们的第三个例子中,关键文件是HTML文件本身,CSS和JavaScript。异步脚本不算在内。这个指标当然越小越好。
再看看关键路径长度
现在你可能会认为这是关键路径最长的大小?我是指我们只需要下载HTML,CSS和JavaScript来渲染我们的页面,而且我们在两次请求中就完成了。
HTTP1文件限制
由于HTTP1协议,我们的浏览器一次只能同时从一个域下载一定数目的文件。它的范围从老浏览器的2个到最多的10个,Chrome可以处理6个。
你可以在这里(http://sgdev-blog.blogspot.co.uk/2014/01/maximum-concurrent-connection-to-same.html)查看用户浏览器可以从一个域并发请求的最大文件数。
你可以,也应该通过从影子域名中获取资源来绕过这一限制,从而最大优化你的潜力。警告:除了您的根域外,不要从其他路径提供关键的CSS,DNS查询和单独延会抵消其他任何情况下可能的好处。
HTTP2
如果你的网站是HTTP2,并且你用户的浏览器是兼容的,那么你完全不受这个限制。但是,这些美好的事情并不常见。
你可以在这里(https://tools.keycdn.com/http2-test)测试你网站的HTTP2。
TCP往返限制
另一个敌人逼近!
在任何一个请求中可传输的最大数据量为14kb,对于包括HTML,CSS和脚本在内的所有Web请求都是如此。这来自防止网络阻塞和丢包的TCP规范(https://hpbn.co/building-blocks-of-tcp/#slow-start)。
如果我们的HTML或请求中的任何资源大于14kb,那么我们需要进行额外的请求来获取它们。所以,是的,这些巨大的文件确实为我们的CRP增加了许多路径。
重要的事
现在让我们处理大规模的网页。
现在我知道一个按钮有很多CSS和JavaScript,但它是非常重要的按钮,对我们来说意义重大。所以让我们不要批判,好吗?
我们的整个HTML很好地缩减压缩,大小为2kb,低于14kb的限制,所以它回在CRP的一次请求中就很好得到,浏览器尽职地利用这一个关键文件开始构建我们的DOM。
CRP指标:长度1,文件1,字节2kb
它获取一个CSS文件,感谢预加载扫描器识别所有外部资源(CSS和JavaScript),并发起一个请求开始下载它们。但等一下,第一个CSS文件是14kb,是一次请求的最大有效载荷,所以它自己是一个CRP。
CRP指标:长度2,文件2,字节16kb
然后下载其他资源。其余的重量在14kb以下,所以可以一次请求得到,但因为共有7个文件,我们的网站还不是HTTP2,并且我们使用的是Chrome,我们只能在这个请求中下载6个文件。
CRP指标:长度3,文件8,字节28kb
现在我们终于可以下载最后一个文件并渲染DOM。
CRP指标:长度4,文件9,字节30kb
现在我们的CRP共有9个关键文件的30kb关键资源和4个关键路径。通过这些信息和一些链接中的延迟的知识,你实际上可以开始对给用户的页面进行非常准确的性能估计。
浏览器网络优化策略
Pagespeed Insights
使用Insights(https://developers.google.com/speed/pagespeed/insights/)来定位性能问题。 Chrome 的开发者工具中也有一个audit标签。
熟练掌握Chrome开发者工具
DevTools是非常非常惊人的。我可以为它们单独写一本书,但现在已经有很多资源可以帮助你。这有一个非常有用的文章(https://developers.google.com/web/tools/chrome-devtools/network-performance/understanding-resource-timing),用于了解网络资源。
建立良好的环境,在不好的环境中测试
你当然希望用1tb SSD和32gb内存的Macbook Pro来构建,但是对于性能测试,如果你用的是Chrome,你应该转到Chrome中的网络选项卡,并且模拟低带宽,限制CPU连接来真正获得一些有用的观察。
将资源/文件放在一起
所以在上述CRP图中,我省略了一些你不需要知道的东西。但基本上,在接收到每个外部CSS和JavaScript文件之后,浏览器构造CSSOM并在上面执行脚本。所以即使你可以在一个往请求中传递多个文件,它们都会浪费浏览器的宝贵时间和资源,因此在可行时最好将文件合并在一起,消除不必要的加载。
放在头部的首屏内部样式
是否通过内化或内联CSS和JavaScript停止从外部加载资源并不是不是绝对的,相反,使用外部资源,以便它们可以利用缓存让DOM轻便。
但是,为首屏数据设置内部样式有一个非常好的理由,第一次绘制可以避免加载资源。
压缩/减小图片
这是很简单和基本的,有很多做这个的工具,选择你最喜欢的。
延迟加载图片到页面加载完成
使用一些简单通用的JavaScript,你可以延迟加载显首屏以下的图片,或着对用户第一个响应不重要的部分。在这里(https://varvy.com/pagespeed/defer-images.html)有一个这么做的很好策略。
异步加载字体
字体加载非常耗资源,如果可以你应该使用网络字体应急,然后逐渐渲染字体和图标表。这可能看起来不是很好,但另一种选择是,如果字体未加载,页面将不会加载任何文本,这称为闪烁的隐形文本(Flash Of Invisible Text, FOIT)。
你真的需要所有的JavaScript / CSS吗?
你是否全都需要?回答我!是否有原生HTML元素可以提供你使用脚本来实现的表现?是否可以通过内联而不是内部/外部创建样式或图标?例如。内联一个SVG(https://css-tricks.com/using-svg/#article-header-id-7)。
CDNs
内容分发网络(Content Delivery Networks,CDNs)可用从离用户提供物理上更近和更低延迟的位置提供资源,从而减少加载时间。
现在你十分开心,知道足够知识从而走出去自己发现关于这个问题更多的内容。我建议学习这个免费的Udacity课程(https://classroom.udacity.com/courses/ud884/lessons/1464158641/concepts/14734291220923),并阅读Google自己的优化文档(https://developers.google.com/web/fundamentals/performance/)。 如果你渴望获得更基本的知识,那么这本免费的高性能浏览器网络(https://hpbn.co/)电子书是开始的好地方。
总结
关键渲染路径非常重要,它为我们提供了一些非常实用和合乎逻辑的规则来优化我们的网站。重要的3个指标是:
1.关键字节数。
2.关键文件数。
3.关键路径数。
我写的这篇文章应该给你提供了足够的基础知识,并帮助你解释Google的 Pagespeed Insights对你的性能有什么评价。
最佳实践的应用应该是将良好的DOM结构,网络优化和可用于减少CRP度量的各种策略的结合。使用户更满意,也让Google的搜索引擎更满意。
在任何企业级规模的网站上,这将是一项艰巨的任务,但是你迟早要做这一点,所以不要再承担更多的技术债务,开始投入可靠的性能优化策略。
如果你读到这里,非常感谢你阅读这么久。我非常希望这篇文章可以帮助你,如果你对我有任何反馈或更正,请发给我。
在接下来的博客中,我希望给出实际的例子,即在我自己团队庞大的代码库中实践所有这些原则。我们在一个域上拥有超过2,000个可管理内容的页面,并且每天有数十万的浏览量,因此这将很有趣。