原文链接:Optimize resource loading, from web.dev。翻译时有删改。
在上一篇文章中,我们讨论了关键渲染路径,了解了影响页面初始渲染效率的阻塞渲染 CSS 和 阻塞解析 JavaScript。
页面加载时,会伴随很多资源的引用。这些资源可能是为页面提供外观和布局的 CSS,也可能是提交页面交互效果的 JavaScript。
本文,我们将深入了解这 2 类资源的阻塞原理并具体学习一些优化手段。
阻塞渲染
从之前的学习,我们知道 CSS 属于阻塞渲染的资源,在浏览器完成下载和解析 CSS 成为 CSSOM 之前,会停止渲染工作。
之所以将 CSS 作为阻塞渲染的资源,是为了避免页面出现短暂的无样式闪现。类似下面这样:
有一个术语专门用于描述这个场景,叫 FOUC,全称是"Flash of Unstyled Content"。
因为 CSS 会作为阻塞渲染的资源处理,FOUC 现象你通常是看不到的,但要理解这个概念,就能明白浏览器为什么要这么做了。
阻塞解析
从之前的学习,我们知道浏览器在遇到 <script>
元素时,会阻塞浏览器进一步的解析 ,优先下载、处理和执行 JavaScript,在处于其余部分的 HTML。
JavaScript 的阻塞解析,也是浏览器刻意为之的策略。这是因为 JavaScript 代码中可能会包含对 DOM 结构的访问和修改。
html
<!-- This is a parser-blocking script: -->
<script src="/script.js"></script>
当 <script>
元素没有注明是 async/defer 的情况下,浏览器会先终止后续解析,优先下载(可选,适应于引用外部资源)、解析、执行 JavaScript 文件。直到这一过程结束,浏览器才继续后续内容的解析。
值得注意的是,JavaScript 的执行也有一个前提,就是当前没有正在处理的 CSS 资源,这也是刻意为之。
因为 CSS 解析时,并不会阻止浏览器解析 JavaScript,如果 JavaScript 中有类似 element.getComputedStyle()
代码调用,那么必然要等到样式解析完成才行,否则是不准确的。因此,JavaScript 的执行要等待 CSS 解析彻底完成。
预加载扫描器
预加载扫描器(preload scanner) 是浏览器的一个优化手段,它是主 HTML 解析器(primary HTML parser)之外的另一个 HTML 解析器,叫辅助 HTML 解析器(secondary HTML parser)。
预加载扫描器会在主 HTML 解析器发现资源之前查找并获取资源。比如:提前下载 <img>
元素中指定的资源。这个操作即便在 HTML 解析器被 JavaScript 和 CSS 阻塞也是如此。
不过预加载扫描器也有一些处理盲区。盲区之内引用的资源无法被识别,也就无法优化了。这些处理盲区包括:
- CSS 中加载的图片资源(通过
background-oimage
属性) - 通过 JavaScript 代码或 import() 动态创建的 DOM 节点
- 客户端渲染 HTML,这类典型就是 SPA 应用
- CSS
@import
声明
以上场景都有一个共同特点,就是资源加载都是滞后的,自然就无法被预加载扫描器知道。
当然,针对这类场景我们还能使用 preload
hint 手动提示来支持。不过这是下一篇的内容了,这里先不赘述。
CSS
CSS 影响页面的外观和布局效果,它是一种阻塞渲染的资源。本小节,我们就来看看如果优化 CSS,来改善页面加时间。
压缩
通过压缩 CSS 减少文件大小,从而加快下载速度。
demo:压缩前
css
/* Unminified CSS: */
/* Heading 1 */
h1 {
font-size: 2em;
color: #000000;
}
/* Heading 2 */
h2 {
font-size: 1.5em;
color: #000000;
}
demo:压缩后
css
/* Minified CSS: */
h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}
压缩是 CSS 最基本而且有效的一个优化手段,可以提高网站 FCP 甚至 LCP 指标。一般前端工程中是配置的打包工具(bundler)都内置了这个功能。
移除无用 CSS
在浏览器渲染网页内容前,需要下载并解析所有样式。当然,这里花费的时间还包括当前页面未使用的样式。如果你使用的打包工具将所有 CSS 资源组合到一个文件中,那么你的用户可能会下载比实际当前渲染页面所需要的更多 CSS。
要发现当前页面未使用的 CSS,可以使用 Chrome DevTools 中的 Coverage 工具。
删除未使用的 CSS 会带来 2 个好处:
- 减少下载时间
- 优化渲染树构造。减少浏览器需要处理的 CSS 规则
避免 CSS @import
声明
虽然看起来很方便,但您应该避免在 CSS 中使用 @import
声明:
css
/* Don't do this: */
@import url('style.css');
与 HTML 中 <link>
元素的工作方式类似,CSS 中的 @import
声明能让你从样式表中导入外部 CSS 资源。
这两种方法之间的主要区别在于 HTML <link>
元素是 HTML 响应的一部分,因此比通过 @import
声明下载的 CSS 文件能更快比发现。
原因在于 @import
声明必须先下载包含 CSS 文件。这会产生所谓的请求链(request chain),它会延迟页面初始渲染所需的时间。另一个缺点,是使用 @import
声明加载的样式表无法被预加载扫描器发现,也会成为后期发现的渲染阻塞资源。
html
<!-- Do this instead: -->
<link rel="stylesheet" href="style.css">
在大多数情况下,你可以使用 <link rel="stylesheet">
元素替换 @import
。<link>
元素是同时下载样式表的,这样能减少总体加载时间,这与 @import
声明连续下载样式表的策略也是不一样的。
内联关键 CSS
所谓"关键 CSS"是指"首屏"页面内容所需要的样式。
下载 CSS 文件需要时间,这会增加页面的 FCP 指标。如果在文档 <head>
中内联关键样式可以消除对 CSS 资源的网络请求。剩余的 CSS 可以异步加载,或者附加在 <body>
元素的末尾。
html
<head>
<title>Page Title</title>
<!-- ... -->
<style>h1,h2{color:#000}h1{font-size:2em}h2{font-size:1.5em}</style>
</head>
<body>
<!-- Other page markup... -->
<link rel="stylesheet" href="non-critical.css">
</body>
另一方面,内联大量 CSS 会向初始 HTML 响应增加更多字节。而通过 HTML 资源通常无法缓存很长时间(或根本不缓存),因此这个也需要我们基于自身情况做选择。
JavaScript
JavaScript 驱动了网络上的大部分交互,但也付出了代价。
传送太多的 JavaScript 可能会让页面加载时间太长、响应缓慢,交互也变慢了,这两种情况都会让用户抓狂。
阻塞解析的 JavaScript
当网页中使用的 <script>
元素不带 defer
或 async
时,当前代码的解析会阻止浏览器后续工作的进行,直到当前代码处理完成。同样的,内联脚本也会阻塞解析,直到脚本代码处理完成。
async
和 defer
<script>
的 async
和 defer
attribute 可以让你在加载外部脚本同时不阻塞 HTML 解析器的工作。不过,async
和 defer
策略上还是有一些不同。
来源自 html.spec.whatwg.org/multipage/s...
使用 async
加载的脚本在下载后立即解析并执行;而使用 defer
加载的脚本在 HTML 文档解析完成时执行------跟 DOMContentLoaded
事件一个时机。
另外,当网页中同时存在多个脚本时。 async
脚本可能无法保证执行执行,而 defer
脚本则会始终按照它们在源代码中出现的顺序执行。
值得注意的是:带有 type="module"
的脚本(包括内联脚本)效果跟 defer
一样;而通过脚本动态注入的 <script>
标签效果类似 async
。
避免客户端渲染
一般来说,你应该避免使用 JavaScript 来呈现任何关键内容或页面的 LCP 元素。这种做法称为"客户端渲染",是单页应用程序 (SPA) 中广泛使用的一种技术。
通过 JavaScript 渲染的 HTML 标签是无法被预加载扫描器观察到的。这可能会延迟关键资源的下载,例如 LCP 图片。浏览器只会在脚本执行后才开始下载 LCP 图片,并添加到 DOM 中,这应该被避免。
此外,与直接在服务器响应请求返回相比,使用 JavaScript 渲染标签更有可能产生较长的任务。广泛使用 HTML 客户端渲染也会延迟交互,在页面 DOM 非常大的情况下尤其如此。
压缩
与 CSS 类似,压缩 JavaScript 可以减少脚本文件大小,加快下载速度,让浏览器更快解析和编译 JavaScript。
此外,JavaScript 的压缩可以比 CSS 等其他资源更好。当压缩 JavaScript 时,它不仅能删除空格、制表符和注释等内容,而且 JavaScript 中标识符也能被缩短,这个过程有时被称为丑化(uglification)。
js
// Unuglified JavaScript source code:
export function injectScript () {
const scriptElement = document.createElement('script');
scriptElement.src = '/js/scripts.js';
scriptElement.type = 'module';
document.body.appendChild(scriptElement);
}
js
// Uglified JavaScript production code:
export function injectScript(){const t=document.createElement("script");t.src="/js/scripts.js",t.type="module",document.body.appendChild(t)}
观察可以发现,你可以看到源代码中变量 scriptElement
被缩短成 t
了。当你脚本代码很多时,这种做法可以节省的体积相当可观,而且也不会影响网站的生产环境功能。
如果你有在用打包工具处理网站源码,JavaScript 的生产压缩默认就有。这类丑化工具------例如 Terser------也是高度可配置的,你可以调整丑化算法的激进程度来实现最大程度的压缩。然而,任何丑化工具的默认设置通常就足够使用的了。
总结
本文在上一篇关键渲染路径的基础上,进一步讨论了浏览器针对 JavaScript/CSS 采用的不同阻塞策略的原因,继而给出优化 JavaScript、CSS 的不同手段。希望对各位正在阅读的朋友日后的工作带来一些帮助。
下一篇里,我们会继续探讨另一种在网页中进行手工优化的方式------资源提示(resource hints),敬请期待。
再见。