Lighthouse 是谷歌开源的一款 Web 前端性能测试工具,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。本文中仅对 Performance 部分的指标进行介绍。
Performance 项的总和得分由6个指标的性能按一定比例综合计算得到。下面是Lighthouse 性能分析的分值报告图例:
为了方便了解各项指标的分值占比、各项指标数据与得分的关系,可使用 Lighthouse Scoring Calculator
进行查看:
首次内容渲染,简称 FCP。测量在用户导航到您的页面后浏览器呈现第一段 DOM 内容所需的时间。1.8 秒内达到快速级别。
确保文本在 webfont 加载期间保持可见。当网页使用自定义字体时,字体文件通常都是较大文件,需要一段时间才能加载完成,某些浏览器会在字体加载之前隐藏文本,从而导致不可见文本闪烁(FOIT)。
避免 FOIT 最简单方法是临时显示系统字体,font-display: swap
告诉浏览器使用自定义字体的文本应立即使用系统字体显示,自定义字体准备就绪后再替换系统字体(遗憾的是 swap
会导致重排)。
@font-face {font-family: 'Pacifico'; font-style: normal;font-weight: 400; src: local('Pacifico Regular'), local('Pacifico-Regular'), url(https://fonts.gstatic.com/s/pacifico/v12/FwZY7-Qmy14u9lezJ-6H6MmBp0u-.woff2) format('woff2'); font-display: swap;
}
消除阻塞渲染的资源。报告中会列出阻止当前页面首次绘制的所有 URL。通过内联关键资源、推迟非关键资源和删除任何未使用的内容来减少这些阻止渲染的 URL 的影响。
以下情况的脚本和样式表将会阻塞渲染:
中没有 defer
和 async
属性的
标签。disabled
属性 或含有 media="all"
的
标签
// media 默认为 "all"
使用 Chrome DevTools 中的 Coverage 选项卡 来识别非关键 CSS 和 JS。该选项卡会告诉你加载了多少代码、其中未实际使用到的代码。
绿色表示首次绘制所需的脚本/样式,即关键资源,红色则为非关键资源。
识别出关键资源后就需要区分出关键与非关键资源。对于 CSS 资源可以使用 Critical、CriticalCSS 或 Penthouse 进行提取,而 JS 资源通常结合如 Webpack 之类的打包工具对非关键资源进行拆分或懒加载👇🏻。
@babel/preset-env
是一个智能预设,指定目标环境后即可使用最新的 JavaScript 特性,它会去管理需要哪些插件以及 polyfill。
@babel/plugin-transform-runtime
可以复用 Babel 注入的辅助代码以减少代码体积;另一个作用可以避免全局注入 polyfill。
内联的方式加载,比如 webpack 打包产物中的运行时资源(runtimeChunk
),其中包含了 webpack 进行模块解析、加载、模块清单等代码。runtimeChunk
每次构建都会发生变化,单独提取出来可以避免非变更 chunk 的 contenthash 变更,从而更好的利用浏览器缓存。但这些代码通常只有几 KB 甚至更小,为此增加网络资源请求十分浪费,通过类似 InlineChunkHtmlPlugin 插件内联到 html 中是更好的选择。
之前HTML 解析过程中如遇到同步的 JS 会等待 JS 下载并执行完之后才继续解析,如果将 JS 资源放在
中可能造成页面持续白屏一段时间。所以当需要兼容一些旧浏览器时将所有的JS脚本都放在
之前是最好的选择。这样可以保证非脚本的其他一切元素能够以最快的速度得到加载和解析。defer
和 async
都可以实现异步加载 JS 资源,async 是无序异步加载,defer 则是有序顺序来异步加载。
async
:并行下载后并直接运行,下载的过程不会阻塞 DOM 解析,但执行会,执行的时间与 DOMContentLoaded 不相关,可能领先也可能在其后。多个 async
JS 无法保证按引入的顺序执行,所以当 JS 资源完全独立时可选择此方式加载。
defer
:并行下载,但下载完成后不会立即执行,它们在 DOM 解析完成之后、DOMContentLoaded 之前按照引入顺序依次执行,因此不会阻塞 DOM 解析。
DOMContentLoaded :当初始的 HTML ****文档被完全加载和解析完成之后,
DOMContentLoaded
****事件会被触发,而不必等待样式表,图片等资源完成加载。
Load :当整个页面及所有依赖资源如样式表和图片都已完成加载时,将触发
load
事件。
import moduleA from "library";
form.addEventListener("submit", e => {e.preventDefault();someFunction();
});
// 使用 import()动态导入
form.addEventListener("submit", e => {e.preventDefault();import('library.moduleA').then(module => module.default).then(someFunction()).catch(handleError());
});
实际项目中懒加载所有第三方模块并不是很常见,通常使用 SplitChunksPlugin 等工具将第三方依赖被拆分并打包成一个单独的 vendors chunk 是更好的选择,因为它们不会经常更新,使用 chunkhash
可以保证每次构建 vendor chunk 的文件名不会变更,从而更好的进行持久化缓存。
使用 import()
或 React.lazy()
在路由或组件级别进行模块拆分则是一种更简单的方式。
import * as React from "react";
import { Routes, Route } from "react-router-dom";
const About = React.lazy(() => import("./pages/About"));
export default function App() {return ( }> } />...>}> }/> );
}
还有一些专门进行懒加载的工具库可以使用,如 Loadable components:
// Component Splitting
import loadable from '@loadable/component';
const Home = loadable(() => import('./Home'));
// Library Splitting
const Moment = loadable.lib(() => import('moment'));
中内联到
中。
`link rel="preload" as="style"` 表示异步请求样式表,`this.rel='stylesheet'` 表示完成加载后再以标准方式加载 `styles.css`。而 `this.onload=null` 将事件处理程序置空有助于避免某些浏览器在切换 `rel` 属性时重新调用处理程序。
`noscript`对于不执行 JavaScript 的浏览器,对元素内部样式表的引用可作为兜底。
### 预加载关键资源
打开一个网页时,浏览器从服务器请求 HTML 文档,然后解析其中内容,如其中存在其他引用的资源则会单独发起请求,比如 CSS 中的自定义字体资源。对于那些较晚被解析发现的资源,可能包含了我们认为的关键资源,所以我们希望能告知浏览器提前去请求这些关键资源,从而就可以加快加载过程。通过 `` 预加载资源就可以实现。
浏览器会下载并缓存预加载的资源,以便在需要时立即可用(它不执行脚本或应用样式表)。提供该 as
属性有助于浏览器根据其类型设置预取资源的优先级,设置正确的标头,并确定资源是否已存在于缓存中。此属性接受的值包括 script
,style
,font
,image
等。省略as
属性或使用了其他无效值则等同于XHR 请求,浏览器将不知道它正在获取什么,因此无法确定正确的优先级。它还可能导致某些资源(例如脚本)被获取两次。
某些类型的资源类型如字体,需要以 crossorigin 模式加载,即在 上设置
crossorigin
属性。同时还接受一个
type
属性,该属性包含链接资源的MIME 类型。浏览器使用该type
属性的值来确保资源仅在其文件类型受支持时才被预加载。如果浏览器不支持指定的资源类型,它将忽略。
上面的图例中,Pacifico 字体是在 CSS 中通过@font-face
定义的。浏览器仅在完成下载和解析 CSS 后才开始加载字体文件。使用 将 Pacifico 字体预加载,字体下载与 CSS 并行下载。
关键请求链表示浏览器优先处理和获取的资源的顺序,Lighthouse 会将位于链中的第三层资源标记为你预加载链接的候选名单,并在 Lighthouse 报告的 Opportunities 部分的 Preload key requests
中进行展示:
假设你的页面的关键请求链如下:
index.html
|--app.js |--styles.css |--ui.js
index.html
文件声明,
app.js
运行时会调用fetch()
下载 styles.css
和 ui.js
,所以在下载、解析和执行最后 2 个资源之前页面不会完整显示。如果app.js
下载、解析和执行需要 200 毫秒,那么预加载 styles.css
和 ui.js
就可能潜在节省为 200 毫秒,从而使您的页面加载速度更快。
预加载浏览器发现较晚的重要资源对 FCP 和 TTI 都有较大提升。但是预加载所有内容会适得其反,因此仅预加载最关键的资源很重要。 load
事件发生后大约 3 秒,未使用的预加载会在 Chrome 中触发控制台警告,这也是判断是否预加载了非关键资源的标志。
预加载 CSS 中通过 @font-face
定义的字体资源可确保在下载 CSS 文件之前获取它们。需要注意的是添加crossorigin
属性,否则预加载的字体将被提取两次。
如果已经提取了关键 CSS,则可以将 CSS分为两个部分,首屏所需的关键 CSS 内联在 中,而非关键 CSS 通常使用 JS 延迟加载,在加载非关键 CSS 之前等待 JS 执行会导致用户滚动时呈现延迟,因此使用 可以更快的启动下载。
由于浏览器不执行预加载的资源,因此预加载有助于将获取与执行分开,这可以改善交互时间等指标。如果能拆分出仅预加载关键 JS 资源,则预加载效果最佳。
增加 webpackPreload: true
注释即可注入预加载标签。
import(/* webpackPreload: true */ "CriticalChunk")
:低优先级资源声明,允许浏览器在空闲时获取并缓存资源。通常对首屏没有帮助,但在能预测用户下一步动向时,prefetch
能加快下一页面加载速度。
:与 prefetch
类似,区别在于 prerender
会在空闲时获取 href
属性页面的所有资源。因为它将会加载很多资源并且可能造成带宽的浪费,所以在移动设备中尤其需要小心使用。
:允许浏览器在一个 HTTP 请求正式发给服务器前进行预连接。包括 DNS 解析,TLS 协商,TCP 握手等。
:更快地完成 DNS 查找。当页面通过网址请求服务器时需要先通过 DNS 解析拿到 IP 地址才能发起请求。如果网站存在大量跨域的资源,DNS 的解析过程很可能会降低页面的性能。对于关键的跨域资源,推荐使用 dns-prefetch
进行 DNS 预获取。还可配合 preconnect
进行预连接。当页面通过网址请求服务器的时候,需要先通过 DNS 解析拿到 IP 地址才能发起请求。如果网站存在大量跨域的资源,DNS 的解析过程很可能会降低页面的性能。对于关键的跨域资源,我们最好进行 dns 预获取,还可以结合 preconnect
进行预连接。
可交互时间,简称TTI。它表示页面完全交互所需的时间。当页面显示了有用的内容(即FCP),大多数可见的页面元素注册了事件处理程序,并且页面能在 50 毫秒内响应用户交互即可称之为可完全交互。通常 TTI 在 3.8 秒内可达到快速等级。
TTI 直接受 JS 脚本的影响,脚本越多,TTI 的延迟越大。
requestAnimationFrame
或 requestIdleCallback
),或者选择在主线程之外(如Web Workers)运行 JavaScript。详见最小化主线程工作👇🏻。通常基于路由进行代码拆分,详见代码拆分、懒加载👆🏻。
为了减少 JavaScript 解析/编译及网络传输时间,通常使用路由分块或 PRPL 等模式。
存在延迟 First Paint 的资源,Lighthouse 会发出警告。
为了改进 First Paint,可以内联关键 JavaScript/ CSS,并推迟剩余资源。这种获取阻塞渲染的资源的方式可以避免关键资源与服务器的往返,从而提高性能。但是内联代码从开发者的角度来看更难维护,并且不能被浏览器单独缓存。另一种方式是通过服务端渲染来呈现初始的 HTML,相对的它会增加 HTML 的文件体积。
Service Workers 通过在客户端和服务端之间充当代理的角色,使得客户端可以直接从缓存中获取资产,而不是在重复访问时从服务器中获取。这不仅允许用户在弱网甚至离线时使用应用程序,而且还可以在重复访问时显著加载时间、提升页面加载速度。
相比自己编写自定义的 Service Worker 来更新预缓存资源,使用第三方库来生成 Service Worker 更加方便,如 Workbox 提供了一组工具,方便我们创建和维护 Service Worker 来缓存资产。
参考 Create React APP 中离线缓存策略。
Third-party usage
项中会显示页面中所有的第三方脚本。Reduce JavaScript execution time
项会显示需要很长时间来解析、编译或执行的脚本,勾选 3rd-party resources
显示第三方脚本。打开 DevTools 中的 Network 选项卡,对其中的任意资源请求右键选择 ****Block request URL
,所有被阻止的请求都将出现在 Request blocking
中。