本文转载自:众成翻译
译者:网络埋伏纪事
连接:http://www.zcfy.cc/article/2847
原文:https://hackernoon.com/optimising-the-front-end-for-the-browser-f2f51a29c572#.81dkyz4uujavascript
优化全都是与速度和满意度有关。css
从用户体验的角度,咱们但愿前端提供能够快速加载和执行的网页。html
而从开发者体验的角度,咱们但愿前端是快速、简单而规范的。前端
这不只会给咱们带来快乐的用户和快乐的开发者,并且因为 Google 偏向于优化,SEO 排名也会显著提升。html5
若是你已经花费了大量时间来改善你网站的 Google Pagespeed Insights分数,那么这将有助于揭示这一切实际上意味着什么,以及咱们必须为优化前端所采起的大量策略。java
最近个人整个团队有机会花一些时间加快把咱们提出的升级变为代码库,多是用 React。这确实让我思考起了咱们该如何建立前端。很快,我意识到浏览器将是咱们的方法中的一个重要因素,同时也是咱们知识中的大瓶颈。node
咱们不能控制浏览器或者改变它的行为方式,可是咱们能够理解它的工做原理,这样就能够优化咱们提供的负载。react
幸运的是,浏览器行为的基础原理是至关稳定并且文档齐全的,而且在至关长一段时间内确定不会发生显著变化。git
因此这至少给了咱们一个目标。github
另外一方面,代码、技术栈、架构和模式是咱们能够控制的东西。它们更灵活,变化的更快,并给咱们这一边提供了更多选择。
我决定从外向内着手,搞清楚咱们代码的最终结果应该是什么样的,而后造成编写代码的意见。在这第一篇博文中,咱们打算专一于对于浏览器来讲咱们须要了解的一切。
下面咱们来创建一些知识。以下是咱们但愿浏览器要运行的一些简单 HTML:
<!DOCTYPE html> <html> <head> <title>The "Click the button" page</title> <meta charset="UTF-8"> <link rel="stylesheet" href="styles.css" /> </head> <body> <h1> Click the button. </h1> <button type="button">Click me</button> <script> var button = document.querySelector("button"); button.style.fontWeight = "bold"; button.addEventListener("click", function () { alert("Well done."); }); </script> </body> </html>
当浏览器接收到 HTML 时,就会解析它,将其分解为浏览器所能理解的词汇,而这个词汇因为HTML5 DOM 规范定义,在全部浏览器中是保持一致的。而后浏览器经过一系列步骤来构造和渲染页面。以下是一个很高层次的概述:
使用 HTML 建立文档对象模型(DOM)。
使用 CSS 建立 CSS 对象模型(CSSOM)。
基于 DOM 和 CSSOM 执行脚本(Script)。
合并 DOM 和 CSSOM 造成渲染树(Render Tree)。
使用渲染树布局(Layout)全部元素的大小和位置。
绘制(Paint)全部元素。
浏览器开始从上到下读取标记,而且经过将它分解成节点,来建立 DOM 。
样式在顶部,脚本在底部
虽然这个规则有例外和细微差异,不过整体思路是尽量早地加载样式,尽量晚地加载脚本。缘由是脚本执行以前,须要 HTML 和 CSS 解析完成,所以,样式尽量的往顶部放,这样在底部脚本开始编译和执行以前,样式有足够的时间完成计算。
下面咱们进一步研究如何在优化的同时作细微调整。
最小化和压缩
这适用于咱们提交的全部内容,包括 HTML、CSS、JavaScript、图片和其它资源。
最小化是移除全部多余的字符,包括空格、注释、多余的分号等等。
压缩(好比 GZip)是将代码或者资源中重复的数据替换为一个指向原始实例的指针,大大压缩下载文件的大小,而且是在客户端解压文件。
左右开弓的话,能够潜在将负载下降 80% 到 90%。好比:光 bootstrap 就节省了 87% 的负载。
可访问性
虽然可访问性不会让页面的下载变得更快,可是会大大提升残障人士的满意度。要确保是给全部人提供的!给元素加上 aria
标签,给图片提供 alt
文本,以及全部其它好东西。
使用像 WAVE 这样的工具确认在哪些地方能够改善可访问性。
当浏览器发现任何与节点相关的样式时(即外部样式表、内部样式表或行内样式),就当即中止渲染 DOM ,并用这些节点来建立 CSSOM。这就是人们称 CSS 阻塞渲染的缘由。这里是不一样类型样式的一些优缺点。
//外部样式 <link rel="stylesheet" href="styles.css"> // 内部样式 <style> h1 { font-size: 18px; } </style> // 行内样式 <button style="background-color: blue;">Click me</button>
CSSOM 节点的建立与 DOM 节点的建立相似,随后,二者会被合并。这就是如今它们的样子:
CSSOM 的构建会阻塞页面的渲染,所以咱们想在树中尽量早地加载样式,让它们尽量轻便,而且在有效的地方延迟加载它们。
使用 media 属性
media 属性指定要加载样式必须知足的条件,好比:是最大仍是最小分辨率?是面向屏幕阅读器吗?
桌面是很强大,可是移动设备不是,因此咱们想给移动设备尽量最轻的负载。咱们能够假设先只提供移动端样式,而后对桌面端样式放一个媒体条件。虽然这样作不会阻止桌面端样式下载,不过会阻止它阻塞页面加载和使用宝贵的资源。
// 这个 css 在全部状况都会下载,并阻塞页面的渲染。 // media="all" 是默认值,而且与不声明任何 media 属性同样。 <link rel="stylesheet" href="mobile-styles.css" media="all"> // 在移动端,这个 css 会在后台下载,并且不会中断页面加载。 <link rel="stylesheet" href="desktop-styles.css" media="min-width: 590px"> // CSS 中只为打印视图计算的媒体查询 <style> @media print { img { display: none; } } </style>
延迟加载 CSS
若是咱们有一些样式能够等到首屏有价值的内容渲染完成后,再加载和计算,好比出如今首屏如下的,或者页面变成响应式以后不那么重要的东西。咱们能够把样式加载写在脚本中,用脚本等待页面加载完成,而后再插入样式。
<html> <head> <link rel="stylesheet" href="main.css"> </head> <body> <div class="main"> 折叠内容之上的重要部分。 </div> <div class="secondary"> 折叠内容之下。页面加载以后,向下滚动才会看到的东西。 </div> <script> window.onload = function () { // 加载 secondary.css } </script> </body> </html>
较小的特殊性
链在一块儿的元素越多,天然要传输的数据就越多,于是会增大 CSS 文件,这是一个明显的缺点。不过这样作还有一个客户端计算的损耗,即会把样式计算为较高的特殊性。
// 更具体的选择器 == 糟糕 .header .nav .menu .link a.navItem { font-size: 18px; } // 较不具体的选择器 == 好 a.navItem { font-size: 18px; }
只加载须要的样式
这听起来可能有点愚蠢或者装模做样,不过若是你已经从事前端工做多年的话,就会知道 CSS 的一个最大问题是删除东西的不可预测性。设计的时候它就是被下了不断增加这样的诅咒。
要尽量多削减 CSS ,可使用相似uncss)包这样的工具,或者若是想有一个网上的替代品,那就处处找找,仍是有挺多选择的。
而后,浏览器会不断建立 DOM / CSSOM 节点,直到发现任何 JavaScript 节点,即外部或者行内的脚本。
// 外部脚本 `<script src="app.js">`</script> // 内部脚本 <script> alert("Oh, hello"); </script>
因为脚本可能须要访问或操做以前的 HTML 或样式,咱们必须等待它们构建完成。
所以浏览器必须中止解析节点,完成建立 CSSOM,执行脚本,而后再继续。这就是人们称 JavaScript 阻塞解析器的缘由。
浏览器有种称为'预加载扫描器'的东西,它会扫描 DOM 的脚本,并开始预加载脚本,不过脚本只会在先前的 CSS 节点已经构建完成后,才会依次执行。
假如这就是咱们的脚本:
var button = document.querySelector("button"); button.style.fontWeight = "bold"; button.addEventListener("click", function () { alert("Well done."); });
那么这就是咱们的 DOM 和 CSSOM 的效果:
优化脚本是咱们能够作的最重要的事情之一,同时也是大多数网站作得最糟糕的事情之一。
异步加载脚本
经过在脚本上使用 async
属性,能够通知浏览器继续,用另外一个低优先级的线程下载这个脚本,而不要阻塞其他页面的加载。一旦脚本下载完成,就会执行。
`<script src="async-script.js" async>`</script>
这意味着这段脚本能够随时执行,这就致使了两个明显的问题。首先,它能够在页面加载后执行很长时间,因此若是依靠它为用户体验作一些事情,那么可能会给用户一个不是最佳的体验。其次,若是它恰好在页面完成加载以前执行,咱们就无法预测它会访问正确的 DOM/CSSOM 元素,而且可能会中断执行。
async
适用于不影响 DOM 或 CSSOM 的脚本,并且尤为适用于与 HTML 和 CSS 代码无关,对用户体验无影响的外部脚本,好比分析或者跟踪脚本。不过若是你发现了任何好的使用案例,那就用它。
延迟加载脚本
defer
跟 async
很是类似,不会阻塞页面加载,但会等到 HTML 完成解析后再执行,而且会按出现的次序执行。
这对于会做用于渲染树上的脚原本说,确实是一个好的选择。不过对于加载折叠内容之上的页面,或者须要以前的脚本已经运行的脚原本说,并不重要。
`<script src="defer-script.js" defer>`</script>
这里是使用 defer 策略的另外一个好选择,或者也可使用 addEventListener
。若是想了解更多,请从这里开始阅读。
// 全部对象都在 DOM 中,而且全部图像、脚本、link 和子帧都完成了加载。 window.onload = function () { }; // 在 DOM 准备好时调用,在图像和其它外部内容准备好以前 document.onload = function () { }; // JQuery 的方式 $(document).ready(function () { });
不幸的是 async
和 defer
对于行内脚本不起做用,由于只要有行内脚本,浏览器默认就会编译和执行它们。当脚本内嵌在 HTML 中时,就会当即运行,经过在外部资源上使用上述两个属性,咱们只是把脚本抽取出来,或者延迟把脚本发布到 DOM/CSSOM。
操做以前克隆节点
当且仅当对 DOM 执行屡次修改时看到了没必要要的行为时,就试试这招。
先克隆整个 DOM 节点,对克隆的节点作修改,而后用它来替换原始节点,这样可能效率更高。由于这样就避免了屡次重画,下降了 CPU 和内存消耗。这样作还能够避免更改页面时的抖动和无样式内容的闪烁(Flash of Unstyled Content,FOUC)。
// 经过克隆,高效操做一个节点 var element = document.querySelector(".my-node"); var elementClone = element.cloneNode(true); // (true) 也克隆子节点, (false) 不会 elementClone.textContent = "I've been manipulated..."; elementClone.children[0].textContent = "...efficiently!"; elementClone.style.backgroundColor = "green"; element.parentNode.replaceChild(elementClone, element);
请注意,克隆的时候并无克隆事件监听器。有时这实际上恰好是咱们想要的。过去咱们已经用过这种方法来重置不调用命名函数时的事件监听器,并且那时也没有 jQuery 的 .on()
和 .off()
方法可用。
Preload/Prefetch/Prerender/Preconnect
这些属性基本上也实现了它们所作的承诺,并且都棒极了。不过,这些属性都是至关新,还没被浏览器广泛支持,也就是说对咱们大多数人来讲它们实际上不是真正的竞争者。不过,若是你有空的话,能够看看这里和这里。
一旦全部节点已被读取,DOM 和 CSSOM 准备好合并,浏览器就会构建渲染树。若是咱们把节点当成单词,把对象模型当成句子,那么渲染树即是整个页面。如今浏览器已经有了渲染页面所需的全部东西。
而后咱们进入布局阶段,肯定页面上全部元素的大小和位置。
最终咱们进入绘制阶段,真正地光栅化屏幕上的像素,为用户绘制页面。
整个过程一般会在几秒或者十分之一秒内发生。咱们的任务是让它作得更快。
若是 JavaScript 事件改变了页面的某个部分,就会致使渲染树的重绘,而且迫使咱们再次经历布局和绘制。如今浏览器足够智能,会仅进行部分重画。不过咱们不能期望靠这就能高效或者高性能。
话虽如此,不过很显然 JavaScript 主要是在客户端基于事件,并且咱们想让它来操做 DOM,因此它就得作到这一点。咱们只是必须限制它的不良影响。
至此你已经足够认识到要感谢 Tali Garsiel 的演讲。这是 2012 年的演讲,可是信息依然是真实的。她在此主题上的综合论文能够在这里读到。
若是你喜欢迄今为止所读过的内容,但仍然渴望知道更多的底层技术性的东西,那么全部知识的权威就是HTML5 规范。
咱们差很少搞定了,不过请和我多待段时间!如今咱们来探讨为何须要知道上面的全部知识。
本节中,咱们将理解如何才能高效地把渲染页面所需的数据传输给浏览器。
当浏览器请求一个 URL 时,服务器会响应一些 HTML。咱们将从超级小的开始,慢慢增长复杂性。
假如这就是咱们页面的 HTML:
<!DOCTYPE html> <html> <head> <title>The "Click the button" page</title> </head> <body> <h1> Button under construction... </h1> </body> </html>
咱们须要学习一个新术语,关键渲染路径(Critical Rendering Path,CRP),就是浏览器渲染页面所需的步数。以下就是如今咱们的 CRP 示意图看起来的样子:
浏览器发起一个 GET 请求,在咱们页面(如今尚未 CSS 及 JavaScript)所需的 1kb HTML 响应回来以前,它一直是空闲的。接收到响应以后,它就能建立 DOM,并渲染页面。
三个 CRP 指标的第一个是路径长度。咱们想让这个指标尽量低。
浏览器用一次往返来获取渲染页面所需的 HTML,而这就是它所需的一切。所以咱们的关键路径长度是 1,很完美。
下面咱们上一个档次,加点内部样式和内部 JavaScript。
<!DOCTYPE html> <html> <head> <title>The "Click the button" page</title> <style> button { color: white; background-color: blue; } </style> </head> <body> <button type="button">Click me</button> <script> var button = document.querySelector("button"); button.addEventListener("click", function () { alert("Well done."); }); </script> </body> </html>
若是咱们检查一下 CRP 示意图,应该能看到有两点变化。
新增了两步,建立 CSSOM和执行脚本。这是由于咱们的 HTML 有内部样式和内部脚本须要计算。不过,因为没有发起外部请求,关键路径长度没变。
可是注意,渲染没那么快了。并且咱们的 HTML 大小增长到了 2kb,因此某些地方仍是受了影响。
那就是三个指标之二,关键字节数出现的地方。这个指标用来衡量渲染页面须要传送多少字节数。并不是页面会下载的全部字节,而是只是实际渲染页面,并把它响应给用户所需的字节。
不用说,咱们也想减小这个数。
若是你认为这就不错了,谁还须要外部资源啊,那就大错特错了。虽然这看起来很诱人,可是它在规模上是不可行的。在现实中,若是个人团队要经过内部或者行内方式给页面之一提供所需的一切,页面就会变得很大。而浏览器不是为了处理这样的负载而建立的。
看看这篇关于像 React 推荐的那样内联全部样式时,页面加载效果的有趣文章。DOM 变大了四倍,挂载花了两倍的时间,到能够响应多花了 50% 的时间。至关不能接受。
还要考虑一个事实,就是外部资源是能够被缓存的,所以在回访页面,或者访问用相同资源(好比 my-global.css
)的其它页面时,浏览器就用发起网络请求,而是用其缓存的版本,从而为咱们赢得更大的胜利。
因此下面咱们更进一步,对样式和脚本使用外部资源。注意这里咱们有一个外部 CSS 文件、一个外部 JavaScript 文件和一个外部 async
JavaScript 文件。
<!DOCTYPE html> <html> <head> <title>The "Click the button" page</title> <link rel="stylesheet" href="styles.css" media="all"> `<script type="text/javascript" src="analytics.js" async>`</script> // async </head> <body> <button type="button">Click me</button> `<script type="text/javascript" src="app.js">`</script> </body> </html>
以下是如今 CRP 示意图看起来的样子:
浏览器获得页面,建立 DOM,一发现任何外部资源,预加载扫描器就开始介入。继续,开始下载 HTML 中所找到的全部外部资源。CSS 和 JavaScript 有较高的优先级,其它资源次之。
它挑出咱们的 styles.css
和 app.js
,开辟另外一条关键路径去获取它们。不过不会挑出 analytics.js
,由于咱们给它加了 async
属性。浏览器依然会用另外一个低优先级的线程下载它,不过由于它不会阻塞页面渲染,因此也与关键路径无关。这正是 Google 本身的优化算法对网站进行排名的方式。
最后,是咱们最后一个 CRP 指标,关键文件,也就是浏览器渲染页面须要下载的文件总数。在例三中,HTML 文件自己、CSS 和 JavaScript 文件都是关键文件。async
的脚本不算。固然,文件越少越好。
如今你可能会认为这确定就是最长的关键路径吧?个人意思是说要渲染页面,咱们只须要下载 HTML、CSS 和 JavaScript,并且只须要两个往返就能够搞定。
不过,生活依然没那么简单。拜 HTTP1 协议所赐,咱们的浏览器一次从一个域名并发下载的最大文件数是有限制的。范围从 2(很老的浏览器)到 10(Edge)或者 6(Chrome)。
你能够从这里查看用户浏览器请求你的域名时的最大并发文件数。
你能够而且应该经过把一些资源放在影子域名上,来绕开这个限制,从而最大限度地提升优化潜力。
警告:不要把关键的 CSS 放到根域名以外的其余地方,DNS 查找和延迟都会抵消这样作时所带来的任何可能的好处。
若是网站是 HTTP2,而且用户的浏览器也是兼容的,那么你就能够彻底避开这个限制。不过,这种好事并不常见。
能够在这里测试你网站的 HTTP2。
另外一个敌人逼近了!
任何一次往返可传输的最大数据量是 14kb,对于包括全部 HTML、CSS 和脚本在内的全部网络请求都是如此。这来自于防止网络拥堵和丢包的一个 TCP 规范。
若是一次请求中,咱们的 HTML 或者任何累积的资源超过了 14kb,那么就须要多作一次往返来获取它们。因此,是的,这些大的资源确实会给 CRP 添加不少路径。
如今将咱们的大网页倾巢而出。
<!DOCTYPE html> <html> <head> <title>The "Click the button" page</title> <link rel="stylesheet" href="styles.css"> // 14kb <link rel="stylesheet" href="main.css"> // 2kb <link rel="stylesheet" href="secondary.css"> // 2kb <link rel="stylesheet" href="framework.css"> // 2kb `<script type="text/javascript" src="app.js">`</script> // 2kb </head> <body> <button type="button">Click me</button> `<script type="text/javascript" src="modules.js">`</script> // 2kb `<script type="text/javascript" src="analytics.js">`</script> // 2kb `<script type="text/javascript" src="modernizr.js">`</script> // 2kb </body> </html>
如今我知道一个按钮就有不少 CSS 和 JavaScript,可是它是一个很重要的按钮,它对咱们来讲意义重大。因此就不要评判,好吗?
整个页面被很好地最小化和压缩到 2kb,远低于 14kb 的限制,因此咱们又回到正好一次 CRP 往返了,而浏览器开始忠实地用一个关键文件,即咱们的 HTML,来建立 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 总共有 30kb 的关键资源,在 9 个关键文件和 4 个关键路径中。有了这个信息,以及一些关于链接中延迟的知识,咱们实际上就能够开始对给定用户的页面性能进行真正准确的估计了。
Pagespeed Insights
使用Insights 来肯定性能问题。Chrome DevTools 中还有个 audit
标签。
善用 Chrome 开发者工具
DevTools 如此惊人。咱们为它单独写一整本书,不过这里已经有很多资源能够帮助你。这里)有一篇开始解释网络资源的文章值得一读。
在好的环境中开发,在糟糕的环境中测试
你固然想在你的 1tb SSD、32G 内存的 Macbook Pro 上开发,不过对于性能测试,应该转到 Chrome 中的 network
标签下,模拟低带宽、节流 CPU 链接,从而真正获得一些有用的信息。
合并资源/文件
在上面的 CRP 示意图中,我省略了一些你不须要知道的东西。不过基本上,每接收到一个外部 CSS 和 JavaScript 文件后,浏览器都会构建 CSSOM,并执行脚本。因此,尽管你能够在一次往返中传递几个文件,它们每一个也都会让浏览器浪费宝贵的时间和资源,因此最好仍是将文件合并在一块儿,消除没必要要的加载。
在 head 部分为首屏设置内部样式
是让 CSS 和 JavaScript 内部化或者内联,以防止获取外部资源,仍是相反,让资源变成外部资源,这样就能够缓存,从而让 DOM 保持轻量,两者并不是非黑即白。
可是有一个很好的观点是对首屏关键内容设置内部样式,能够避免在首次有意义的渲染时获取资源。
最小化/压缩图片
这很简单、基础,有不少选择能够这样作,选一个你最喜欢的便可。
延迟加载图片直到页面加载后
用一些简单的原生 JavaScript,你就能够延迟加载出如今折叠部分之下或者对首次用户响应状态不重要的图片。这里有一些不错的策略。
异步加载字体
字体加载的代价很是高,若是能够的话,你应该使用带回退的 web 字体,而后逐步渲染字体和图标。这看起来可能不咋样,不过另外一个选择是若是字体尚未加载,页面加载时就彻底没有文字,这被称为不可见文本的闪烁(Flash Of Invisible Text,FOIT)。
是否真正须要 JavaScript/CSS?
你须要吗?请回答我!是否有原生 HTML 元素能够产生用脚本同样的行为?是否能够用行内样式或图标而不是内部/外部资源?好比,内联一个 SVG。
CDN
内容分发网络(CDN)可用于为用户提供物理上更近和更低延迟的位置,从而下降加载时间。
*
如今你开心惨了,已经知道了足够多的东西,能够从这里走出去,本身探索有关这个主题的更多东西了。我推荐参加这个免费的 Udacity 课程,而且阅读Google 本身的 优化文档。
若是你渴望更底层的知识,那么这本免费电子书《高性能浏览器网络》是个开始的好地方。
关键渲染路径是最重要的,它让网站优化有规律可循。须要关注的 3 个指标是:
1 — 关键字节数
2 — 关键文件数
3 — 关键路径数
这里我所写的应该足以让你掌握基础知识,并帮助你解释 Google Pagespeed Insights对你的性能有什么见解。
最佳实践的应用将伴随着良好的 DOM 结构、网络优化和可用于减小 CRP 指标的各类策略的结合。让用户更高兴,让 Google 的搜索引擎更高兴。
在任何企业级网站中,这将是一项艰巨的任务,可是你必须早晚作到这一点,因此不要再承担更多的技术性债务,并开始投资于坚实的性能优化策略。
感谢你阅读至此,若是你真的作到了。衷心但愿能帮到你,有任何反馈或者纠正,请给我发消息。
在本博客的下一部分中,我但愿给出一个真实世界的例子,说明如何在我本身的团队的大量代码库中实现全部这些原则。咱们在一个域上拥有超过 2000 个可管理内容的页面,而且天天为数十万个页面浏览量提供服务,所以这将颇有趣。
不过这可能还须要段时间,我如今须要休息一下。