简介
Lighthouse 是谷歌开源的一款 Web 前端性能测试工具,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。本文中仅对 Performance 部分的指标进行介绍。

Performance 指标
Performance 项的总和得分由6个指标的性能按一定比例综合计算得到。下面是Lighthouse 性能分析的分值报告图例:

为了方便了解各项指标的分值占比、各项指标数据与得分的关系,可使用 Lighthouse Scoring Calculator
进行查看:

First Contentful Paint
首次内容渲染,简称 FCP。测量在用户导航到您的页面后浏览器呈现第一段 DOM 内容所需的时间。1.8 秒内达到快速级别。
如何提升
确保文本在 webfont 加载期间保持可见
确保文本在 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 的影响。
哪些资源会阻塞渲染?
以下情况的脚本和样式表将会阻塞渲染:
<head>
中没有defer
和async
属性的<script>
标签。- 没有
disabled
属性 或含有media="all"
的<link rel="stylesheet">
标签
<link href="style.css" rel="stylesheet" media="all">
<link href="style.css" rel="stylesheet"> // media 默认为 "all"
如何识别关键资源?
使用 Chrome DevTools 中的 Coverage 选项卡 来识别非关键 CSS 和 JS。该选项卡会告诉你加载了多少代码、其中未实际使用到的代码。

绿色表示首次绘制所需的脚本/样式,即关键资源,红色则为非关键资源。

如何提取关键资源?
识别出关键资源后就需要区分出关键与非关键资源。对于 CSS 资源可以使用 Critical、CriticalCSS 或 Penthouse 进行提取,而 JS 资源通常结合如 Webpack 之类的打包工具对非关键资源进行拆分或懒加载👇🏻。
如何消除阻塞渲染的脚本?
-
削减和压缩 JS 文件如果你使用 webpack 等构建工具,添加 TerserWebpackPlugin 对 JS 进行压缩混淆;开启 Tree Shaking 移除未使用的代码;使用 compression-webpack-plugin 进行编码压缩,如果使用了 CDN,通常也存在压缩算法等配置。
-
减少未使用的 polyfill我们通常使用 Babel 对代码编译成指定环境能运行的代码,通过下面的预设/插件可以减少不必要的 polyfill:
-
@babel/preset-env
是一个智能预设,指定目标环境后即可使用最新的 JavaScript 特性,它会去管理需要哪些插件以及 polyfill。 -
@babel/plugin-transform-runtime
可以复用 Babel 注入的辅助代码以减少代码体积;另一个作用可以避免全局注入 polyfill。 -
内联核心脚本实际项目中会把一些小但十分重要的 JS 资源通过
<script> ... </script>
内联的方式加载,比如 webpack 打包产物中的运行时资源(runtimeChunk
),其中包含了 webpack 进行模块解析、加载、模块清单等代码。runtimeChunk
每次构建都会发生变化,单独提取出来可以避免非变更 chunk 的 contenthash 变更,从而更好的利用浏览器缓存。但这些代码通常只有几 KB 甚至更小,为此增加网络资源请求十分浪费,通过类似 InlineChunkHtmlPlugin 插件内联到 html 中是更好的选择。 -
非关键脚本移至
</body>
之前HTML 解析过程中如遇到同步的 JS 会等待 JS 下载并执行完之后才继续解析,如果将 JS 资源放在<head>
中可能造成页面持续白屏一段时间。所以当需要兼容一些旧浏览器时将所有的JS脚本都放在</body>
之前是最好的选择。这样可以保证非脚本的其他一切元素能够以最快的速度得到加载和解析。 -
异步加载其他非关键脚本
defer
和async
都可以实现异步加载 JS 资源,async 是无序异步加载,defer 则是有序顺序来异步加载。
<script src="xx.js" async></script>
<script src="xx.js" defer></script>
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 (<Routes><Route path="/" element={<Layout />}><Route index element={<Home />} /><Routepath="about"element={<React.Suspense fallback={<>...</>}><About /></React.Suspense>}/></Route></Routes>);
}
还有一些专门进行懒加载的工具库可以使用,如 Loadable components:
// Component Splitting
import loadable from '@loadable/component';
const Home = loadable(() => import('./Home'));
// Library Splitting
const Moment = loadable.lib(() => import('moment'));