Lighthouse 前端性能优化:从一个DEMO项目入手,一步一步提升性能评分

4. 分析评估

在前端项目中,性能优化不是盲目的,而是基于量化的指标和监控数据来判断 哪些地方需要优化 以及 如何优化 。我们可以通过前端性能监控、分析工具和指标数据,找出性能瓶颈,并针对性地进行优化。

1. 什么是前端性能监控量化?

前端性能监控量化 是指使用可量化的指标(如页面加载时间、交互响应速度、资源大小等)来评估网页或应用的性能,并通过监测这些指标的数据趋势,找出可能的性能瓶颈。

常见的性能优化需要关注以下几方面:

  • 页面加载性能(首屏渲染时间、白屏时间、资源加载速度)
  • 交互响应性能(用户操作的延迟、动画流畅度)
  • 代码执行性能(JavaScript 运行速度、计算密集型任务优化)
  • 网络请求优化(HTTP 请求数、请求大小、CDN 加速)
  • 错误和异常监控(JS 报错、网络请求失败)

2. 前端性能监控的核心指标

前端性能监控指标可以分为 页面加载性能指标交互体验性能指标

2.1 页面加载性能指标
指标 说明 重要性
TTFB(Time to First Byte) 从用户发起请求到服务器返回第一字节的时间 🔥🔥🔥
FCP(First Contentful Paint) 页面首次渲染内容出现的时间 🔥🔥
LCP(Largest Contentful Paint) 加载最大可视内容(如大图片或大段文本)的时间 🔥🔥🔥
TTI(Time to Interactive) 页面可以交互的时间 🔥🔥🔥
DOMContentLoaded(DCL) DOM 解析完成的时间 🔥
Load Time 页面所有资源加载完成的时间 🔥

🔹 示例:使用 Performance API 监控 LCP

ini 复制代码
const observer = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  console.log('LCP:', entries[0].startTime);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
2.2 交互体验性能指标
指标 说明 重要性
FID(First Input Delay) 用户首次交互(点击、输入等)与浏览器响应之间的延迟 🔥🔥🔥
CLS(Cumulative Layout Shift) 页面布局的视觉稳定性 🔥🔥
FPS(Frames Per Second) 页面帧率,影响动画流畅度 🔥🔥
TBT(Total Blocking Time) JavaScript 阻塞主线程的时间 🔥🔥🔥

🔹 示例:使用 Performance API 监控 FID

javascript 复制代码
const observer = new PerformanceObserver((entryList) => {
  entryList.getEntries().forEach((entry) => {
    console.log('FID:', entry.processingStart - entry.startTime);
  });
});
observer.observe({ type: 'first-input', buffered: true });

3. 如何进行前端性能监测?

我们可以通过以下方式对前端项目进行监控:

3.1 使用 Chrome DevTools

Chrome DevTools 提供了一整套分析工具:

  1. Network 面板:查看网络请求、资源加载情况。
  2. Performance 面板分析 CPU、JavaScript 执行、帧率等
  3. Coverage 面板:检测未使用的 CSS 和 JavaScript 代码。

🔹 示例:使用 Performance 进行分析

  1. 打开 DevTools(F12Cmd + Option + I)。
  2. 选择 Performance 选项卡。
  3. 点击 Start Profiling and Reload Page 进行录制。
  4. 查看 CPU、网络请求、渲染时间等数据。
3.2 使用 Lighthouse 进行性能评分

Lighthouse 是 Google 提供的开源工具,能够分析前端性能、SEO、可访问性等。

🔹 如何使用 Lighthouse

  1. 在 Chrome DevTools 中运行

    • 打开 DevTools (F12)
    • 进入 Lighthouse 选项卡
    • 点击 "Generate report"
    • 查看 Performance 分数和优化建议
  2. 使用 CLI 运行

    arduino 复制代码
    npx lighthouse https://example.com --view
  3. 使用 PageSpeed Insights

3.3 使用 Web Vitals 监测

Google 提供的 Web Vitals 可以帮助监测 LCP、FID、CLS 等关键指标。

🔹 示例:集成 Web Vitals 进行监控

javascript 复制代码
import { getCLS, getFID, getLCP } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getLCP(console.log);
3.4 使用 Performance API

Performance API 允许我们在 JavaScript 代码中监控关键性能数据。

🔹 示例:获取页面加载时间

javascript 复制代码
window.addEventListener('load', () => {
  const { loadEventEnd, navigationStart } = performance.timing;
  console.log('页面加载时间:', loadEventEnd - navigationStart, 'ms');
});
3.5 使用第三方监控平台

为了监控线上环境的性能,可以使用以下服务:

工具 主要功能
Google Analytics 监测页面性能、用户交互
Sentry 监测错误和异常
New Relic 监测应用运行状态
Datadog 监测前端和后端性能
腾讯云 CLS 监测 Web 应用日志

🔹 示例:使用 Sentry 监控前端错误

vbnet 复制代码
import * as Sentry from '@sentry/browser';

Sentry.init({
  dsn: 'https://[email protected]/your-project-id',
});

try {
  throw new Error('测试异常');
} catch (error) {
  Sentry.captureException(error);
}

4. 总结

如何找到性能问题?

  1. 使用 Chrome DevTools 分析页面加载情况
  2. 使用 Lighthouse 进行自动化评分
  3. 使用 Performance API 监测关键指标
  4. 使用 Web Vitals 监测 LCP、FID、CLS
  5. 使用第三方监控工具(Sentry、New Relic)

通过这些方法,我们可以准确定位性能瓶颈,并进行针对性优化

5.团队管理和规范制定

团队中要建立文档宣贯以及代码审查,明确我们要怎么写代码才能运行得更好。 这个在我的前端工程化的文章中有讲

6. 常见的方案

6.1 骨架屏

待更新...

6.2 虚拟列表

待更新...

我详细记录了如何将 Lighthouse 性能评分从 2分 提升到 99分 的过程。涵盖了代码压缩、图片优化、预加载、懒加载等常见优化技巧。

项目地址:lighthouse-demo

你可以将这个项目克隆到本地,并切到 before_optimization 分支,尝试自己进行优化,看看能将分数提升到多少。然后,跟随这篇文章和 commit 记录,一步一步提升 Lighthouse 评分。

优化措施 Lighthouse 评分 相关 Commit
[初始页面](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-1") 2 分 init: initial project
[开启文本压缩](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-2") 21 分 perf: enable text compression
[优化图片偏移](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-3") 52 分 perf: optimize CLS by setting image size
[延迟加载 JS 脚本](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-4") 57 分 perf: defer loading JS scripts
[减少 JS 执行时间](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-5") 86 分 perf: reduce JS execution time
[分包和提取 CSS 文件](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-6") 86 分 perf: split chunks and extract CSS
[缩减资源体积](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-7") 97 分 perf: reduce resource size
[图片优化](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-8") 99 分 perf: optimize images
[延迟加载 CSS 文件](#优化措施 Lighthouse 评分 相关 Commit 初始页面 2 分 init: initial project 开启文本压缩 21 分 perf: enable text compression 优化图片偏移 52 分 perf: optimize CLS by setting image size 延迟加载 JS 脚本 57 分 perf: defer loading JS scripts 减少 JS 执行时间 86 分 perf: reduce JS execution time 分包和提取 CSS 文件 86 分 perf: split chunks and extract CSS 缩减资源体积 97 分 perf: reduce resource size 图片优化 99 分 perf: optimize images 延迟加载 CSS 文件 99 分 perf: defer loading CSS files "#heading-9") 99 分 perf: defer loading CSS files

0. 本地启动项目

  1. 克隆项目到本地

  2. 打开 Chrome Devtool 工具,切到 Lighthouse 栏,点击分析页面按钮。

等待些许时间就可以看到性能评分了:

  1. 修改代码后,需要重新执行 npm run preview,在进行 lighthouse 性能分析。

1. 开启文本压缩

相关 Commit:perf: enable text compression

在 Lighthouse 评分界面往下滑,就可以发现官方提供的一些诊断建议(DIAGNOSTICS),我们可以参照这些建议进行优化。 如下图,Lighthouse 建议开启文本压缩以缩减网络资源的体积,从而提升资源的加载速度。

打开 Demo 项目我们可以发现,项目是通过 express 启动的服务,因此我们只需要安装 compression 依赖,然后在 server.js 文件通过 app.use 添加文本压缩的中间件 compression 即可。

再次运行 npm run preview,分析页面,结果分数从 2 分提升到 21 分。可以发现跟资源加载速度相关的指标,FCP(首次内容绘制时间)、LCP(最大内容绘制时间)以及 Speed Index(速度指标)都有相应的提升。

2. 优化图片偏移

相关 Commit:perf: avoid large layout shifts

接下来 Lighthouse 诊断出页面存在严重的布局偏移问题,并帮我定位了问题所在的图片元素。

首先简单介绍一下什么是布局偏移,以及相关的 Lighthouse 指标 CLS(累积布局偏移):

布局偏移:当用户浏览网页时,页面上的元素(例如文本、图片、按钮)如果在加载过程中发生了意外位置变化,就会产生布局偏移。这样的情况可能会导致用户体验不佳,例如点击某个按钮时,由于按钮位置变化而点到了别的地方。

CLS:该指标衡量的是网页整个生命周期内所有意外布局偏移的总和CLS 的值越大,说明页面的视觉稳定性越差。

常见的导致布局偏移原因以及解决方案:

  • 图片没有指定尺寸,导致加载时内容位置变化;

    • 解决方案:在 <img> 标签中明确指定 widthheight 属性;
  • 动态加载的内容(如广告)没有预留空间

    • 解决方案:为广告、动态内容等预留固定的占位元素;
  • 使用字体时,字体加载后替换了默认字体;

    • 解决方案:使用 font-display: swap 避免字体加载时的布局抖动。

回到 Demo 项目,Lighthouse 已经帮我们定位到发生布局偏移的图片。查看代码发现:图片没有指定固定的高度属性,我们给 img 标签添加 height 属性即可

优化后分数从 21 分提升到 52 分,与之相关的 CLS 指标从 0.825 降低到了 0。可以看出 LCS 在整个 Lighthouse 性能评分占据着较大的比重(25%)。

3. 延迟加载JS脚本

相关 Commit:perf: defer script

下图中 Lighthouse 诊断出:存在阻塞页面渲染的资源,并且建议延迟加载所有非必要资源( Resources are blocking the first paint of your page. Consider delivering critical JS/CSS inline and deferring all non-critical JS/styles)

在 Lighthouse 面板中,资源对渲染的影响可能并不直观。我们可以借助 Performance 工具对页面进行录制,在录制完成后查看性能报告中的 Main 视图(主线程),来清晰地呈现浏览器的渲染情况:

在上图 Performance 面板我们可以看到,JS 资源的加载和执行阻塞了页面渲染,导致页面首次渲染的时间延后了很长时间。这里涉及到浏览器在加载页面时的一个机制:在解析 HTML 过程中,如果遇到同步的 JS 脚本,会优先加载并且执行它,执行完毕后再去解析 HTML。针对这种情况,我们可以给 script 标签添加 defer 属性,让其异步加载 JS 资源,并且等待 HTML 解析完毕后再去执行 JS 脚本。

回到 Demo 项目,页面的 script 标签是通过 webpackHtmlWebpackPlugin 插件插入 HTML 中的,因此我们需要将插件的 scriptLoading 属性从 blocking 修改为 defer,就可以实现 JS 脚本的延迟加载。

优化后分数从 52 分提升到 57 分,并且与页面渲染速度相关的指标 FCP 和 LCP 都有相应的提升。

4. 减少JS执行时间

相关 Commit:perf: reduce javascript execution time

下图中 Lighthouse 建议减少 JS 的执行时间,由于 JavaScript 是单线程,在处理耗时较长的任务时就没办法及时响应用户操作,从而影响到一个非常重要的指标------TBT(总阻塞时间)

简单介绍一下 TBT 指标:

  • TBT 衡量的是网页未响应用户输入(例如鼠标点击、屏幕点按或键盘按下操作)的总时长。

  • 计算规则:将 FCPTTI 之间所有长任务阻塞部分 相加。(任何执行时间超过 50 毫秒的任务都是长任务。50 毫秒之后的时间属于阻塞部分。例如,如果 Lighthouse 检测到时长为 70 毫秒的任务,则阻塞部分将为 20 毫秒。

  • 常见优化手段:

    • 减少 JavaScript 执行时间,优化业务逻辑代码,去掉无效代码,使用更高效的算法或数据结构,避免重复计算或不必要的操作。
    • 小化主线程工作,将一些耗时的任务分片执行,利用 requestIdleCallback 或 setTimeout 将任务拆分为多个小于50毫秒的子任务,或者将任务移动到 Web Worker 中执行

那么如何定位长任务?我们可以继续使用 Performance 工具 对页面进行录制和分析。若 Main 栏的任务出现红色条,则表明这是个长任务

由于代码经过了打包,导致我们无法直接定位到代码的具体位置,有需求的同学可以通过 source map 精确定位。我们直接来到项目的主组件 src/App.tsx,可以发现这里存在一个复杂计算,每次渲染 App 组件是都会重复进行计算。这个计算值不依赖于任何状态,**所以我们可以直接计算出结果,避免计算导致的长任务耗时。(如果依赖前端状态可以用 useMemo 进行缓存,避免重复计算。) **

优化后分数从 57 分提升到 86 分,TBT 阻塞时间从 9390ms 减少至 190ms,有明显的提升。

5. 分包和提取CSS文件

相关 Commit:perf: split chunks and extract css

在优化完 JS 执行时间后,细心的朋友也许会发现,TBT 阻塞时间还有 190s。我们再次打开 Performance 面板进行分析,发现脚本评估(Evaluate Script)占据着主线程大部分时间,且大于脚本执行(Function call)的时间

  • 脚本评估是什么?脚本评估时,系统会先对其进行解析以检查是否存在错误。如果解析器未发现错误,则脚本会编译为字节码然后可以继续执行。
  • 如何优化脚本评估?我们可以通过拆分脚本,将脚本评估工作分散到许多较小的任务中,从而减少长任务的阻塞时间,降低 TBT 指标。

首先,打开 Network 看一下资源的组成:

可以发现除了图片资源和页面 document 之外,只有一个 JS 资源。我们再来分析一下 JS 资源的构成,打开本地终端,运行脚本分析命令:

arduino 复制代码
npm run analyze

我们发现样式文件和一些第三方 JS 依赖(antd、moment、react-dom等)都被打进 main.js 了,导致 JS 资源的体积较大,有 367 kb。

分离 JS Bundle

首先我们先利用 Webpack 自带的 splitChunks 分包功能,将第三方 JS 依赖分离成单独的 JS Bundle:

在 Webpack 配置文件 scripts/webpack.base.js 中,通过 optimization.splitChunks 属性,我们可以配置 JS 的分包规则:

  • chunks 属性设置为 all,表示对所有(异步&同步)文件提取公共部分,进行分包;
  • cacheGroups 属性用来设置缓存组,可以对特定路径下的文件进行分包。
提取 CSS 文件

除了第三方 JS 依赖之外,目前样式文件也被打进了 main.js 里,我们可以通过 mini-css-extract-plugin 插件将样式文件抽离成单独的 css 文件。

最终,我们将 main.js 文件拆分成了多个单独的 js 文件和 css 文件:

做完分包以及 CSS 文件提取之后,TBT 的阻塞时间从 190ms 降低到 30ms,缩短了脚本评估的时间。但是总分却没有变化,LCP 反而增加了,其原因是单独抽离 CSS 资源阻塞了 HTML 的解析和渲染(后面会详细阐述)。同时资源的总体积并没有缩减,所以 Lighthosue 的分数变化不大。

6. 缩减资源体积

相关 Commit:perf: import antd on demand & perf: dayjs replace momentjs

完成分包后,继续使用 Lighthouse 分析页面:

经过诊断,Lighthouse 建议减少未使用的 JS 和 CSS(Reduce unused JavaScript & CSS),并且定位到具体的位置:antd-icon.jsmoment.jsantd.css

通过 Network 面板,我们也可以直观的看到 antd-icon.jsantd.css 资源体积都比较大。

针对 antd(3.x版本) 我们可以直接通过具体的文件路径引入组件、图标和样式,以实现按需引入:

同时,针对 moment(75.9kb)库,我们可以采用功能基本相同,但是体积更小的 dayjs(2kb)进行替换:

经过优化后,JS 和 CSS 资源的体积都得到了削减:

优化过资源体积后,Lighthouse 评分从 86 分提升到了 97 分,TBT 和 LCP 指标都有显著的提升。

7. 图片优化

相关 Commit:perf: optimize images & perf: preload largest content image

优化完 JS 和 CSS 资源后,Lighthouse 下一步的诊断建议是针对图片进行优化:提供下一代的图片格式以及预加载 LCP 图片(Serve images in next-gen formats & Preload Largest Contentful Paint image)

优化图片格式

目前项目使用的 .jpg 格式的图片,Lighthouse 建议使用 .webp 格式的图片。

WebP 是一种现代图片格式 ,可为网络上的图片提供出色的无损和有损压缩。使用 WebP,网站站长和 Web 开发者可以创建更小、更丰富的图片,从而提高网页加载速度。

我们可以通过线上图片转换网站 JPG to WEBP | CloudConvert 将图片转化为 .webp 格式,然后在项目 assets/images 目录下进行替换。

根据上图,可以看到图片资源的体积在转化为 webp 格式后削减了一半以上,优化效果还是十分明显的。

预加载 LCP 图片

LCP 资源指的是页面中最大的可见内容元素(通常是图片,视频或文本),对于本项目来说,LCP 资源是页面最开头的三张松鼠图片的其中之一。一般来说,我们希望 LCP 资源与在网页加载的第一个资源的同时开始加载。换句话说,如果 LCP 资源的加载时间晚于第一个资源,则说明有改进的空间。

针对本项目,我们可以通过 <link ref="preload" href="..." as="image" /> 标签预加载 LCP 图片资源:

预加载优化完毕,我们再来通过 Performance 面板对比一下优化前后 LCP 资源加载顺序的区别:

优化前,LCP 图片资源在 JS/CSS 资源加载完成后再加载:

优化后,LCP 图片资源与 JS/CSS 同步加载:

最终,经过图片格式优化和预加载优化后,评分从 97 分提高到 99 分, LCP 从 2.6s 缩减到 2.0s。

8. 延迟加载CSS文件

相关 Commit:perf: defer css and split chunks

经过上文一系列的优化,目前页面的性能已经变得十分可观。Lighthouse 的诊断建议只剩下一项:消除阻塞渲染的资源(Eliminate render-blocking resources):

通过诊断信息我们可以知道:CSS 资源 main.css 的加载阻塞了渲染。通过下图 Performance 面板 Network 栏,我们也可以看到 main.css 资源被标记了 Render blocking 标识。

上文中提到过 <script /> 脚本标签可以通过添加 defer 属性实现延迟加载的功能,但对于 <link ref="stylesheet" /> 样式标签实现延迟加载会相对复杂一些,但我们仍然可以通过以下的方式实现:

  1. rel="stylesheet" 修改为 rel="preload",并添加 as="style",这样就可以让浏览器以为这是一个异步加载资源;
  2. 添加 onload="this.onload=null;this.rel='stylesheet'" 回调事件,这样可以在资源加载完后,使浏览器以样式文件的格式进行解析;

又因为 link 标签是由 mini-css-extract-plugin 生成的,但是这个插件不提供修改 link 标签属性的方法,所以为了实现上述的 1、2 步骤,我们需要自己实现一个 Webpack Plugin

实现思路如下:利用 HtmlWebpackPluginbeforeEmit 钩子,获取当前的 HTML 内容,然后给 link 的标签替换上述 1、2 步骤中的属性。

具体实现代码如下(代码地址见本段文章开头相关 Commit):

通过上述比较 Hack 的方案,成功实现了 main.css 资源的延迟加载(defer loading),Render blocking 标识被成功的去除:

最终,Lighthouse 分数定格在 99 分,红色的诊断建议也清理完毕!🎉🎉🎉

总结

通过上述优化过程,我们可以将优化手段分为以下几个方面:

  1. 资源加载优化

    • 开启文本压缩减少资源体积(使用 compression 库);
    • 延迟加载 JS 脚本(使用 script defer 关键字);
    • 延迟加载 CSS 文件(通过修改 link 标签属性);
    • 预加载关键资源(preload LCP 图片);
  2. 图片相关优化

    • 避免图片布局偏移(设置固定尺寸);
    • 使用下一代的图片格式(.jpg 替换为 .webp 格式);
  3. 代码优化

    • 代码分割(使用 Webpack 的 splitChunks 功能)
    • 按需引入(通过具体的文件路径引用组件)
    • 减少代码执行时间(分割长任务和优化昂贵计算)
    • 寻找体积更小的第三方依赖(dayjs 替换 momentjs

通过这些优化手段,我们成功将 Lighthouse 性能评分从 2 分提升到 99 分。具体指标改善包括:

  • FCP(首次内容绘制)从 3.7s 降至 0.8s;
  • LCP(最大内容绘制)从 8.4s 降至 2.0s;
  • CLS(累积布局偏移)从 0.825 降至 0;
  • TBT(总阻塞时间)从 9400ms 降至 0ms;

这些优化不仅提升了性能评分,更重要的是显著改善了用户体验。希望这个示例项目能帮助大家更好地理解和实践网页 Lighthouse 性能优化。

前端性能优化 --- 保姆级 Performance 工具使用指南

性能分析一直是前端er们津津乐道的话题;而俗话说得好:工欲善其事,必先利其器;

说到前端性能,就少不了性能分析的利器 ------ Performance 😎;我想各位前端小伙伴,在使用 Chrome 浏览器 DevTool 时或多或少都有留意到这个面板吧?

今天我们就迈开性能分析的第一步,来探究一下 Performance 这个强大的工具。

文章中提到的 Performance 面板,以 Chrome 118 版本为参考。

Performance 面板

我们打开 Performance 面板:

我把 Performance 面板大概划分为操作区、设置区、报告区三块区域:

  • 操作区:主要用于性能分析的开启、关闭、性能报告的上传下载等;
  • 设置区:用于设置性能数据分析期间的各项指标,比如:CPU 运算能力、网络状态等等;
  • 报告区:顾名思义,就是呈现最终性能分析报告的地方。

下面我们先来一起看看操作区的内容。

操作区

观察下图中用红线框起来的部分,这里有几个按钮:

录制报告

按钮 的功能大致相同,都是用于记录页面性能数据

两个按钮点击后都会出现下面这样的弹窗:

不过需要注意的是,这两种录制方式之间有一些不同:

  • 点击 (Record) 按钮,Chrome 会开始记录交互阶段的性能数据,需要手动点击 Stop 停止录制
  • 而点击 (Start Profiling and reload page) 按钮,Chrome 会重新刷新页面并自动开始记录,等到页面完全渲染出来后自动结束记录

清除报告

录制完成的数据,如果我们不需要了,就可以点击 (Clear) 按钮来清除数据

是不是很一目了然呢?

上传/下载报告

当我们的数据录制完成,就可以 点击 (Save Profile) 按钮来将录制的报告下载保存在本地

有了下载,当然少不了它的好兄弟上传!

点击 (Load Profile) 就可以将本地报告再次上传到 Performance 面板中

报告对比

实际上,我们每次录制的报告都被自动留存起来;

当存在多份性能报告时,还可以通过下拉切换查看不同的报告

这样就能很方便地对比不同时间段的性能报告。

图中用红虚线框起来的部分,就是用于切换报告的下拉框。

Screenshots 和 Memory

在操作区的右边还有两个复选框,ScreenshotsMemory

Screenshots

当勾选 Screenshots 后,在生成的性能报告中就会多出一栏用于记录页面在加载过程中的每一帧的视觉变化,并生成一系列快照;

并且当鼠标 hover 在这个区域时,就可以查看当前帧的快照:

通过查看这些快照,我们就可以了解页面在不同时间点的渲染情况。

Memory

Memory 功能则是用于观察页面的内存使用情况随时间的变化。

当勾选 Memory 功能后,在性能报告中就会显示页面的总内存使用量、堆内存使用量以及各种 JavaScript 对象的内存分配情况:

同样,当鼠标 hover 在下方不同类型文件占据内存情况的折线图时,就可以查看当前时间点下这些文件使用内存的情况

垃圾回收

最后,操作区的 (Collect garbage) 按钮用于手动触发 JavaScript 垃圾回收。

当我们在进行性能测试时,就可以在特定的时间点手动触发垃圾回收,以确保测试结果的准确性。

设置区

在文章前面简单介绍过,设置区主要用于设置性能数据分析期间的各项指标;

下面我们来看看具体可以做哪些设置:

Disable JavaScript samples

默认情况下,Chrome 在做性能分析采样时会记录当前 JavaScript 执行的堆栈信息,并在报告区的 Main 部分呈现出来

如果我们不关注 JavaScript 执行相关的性能,比如:测试在不同网络情况下的代码执行性能、测试渲染性能时;

这种情况下就可以勾选 Disable JavaScript samples 这个选项,从而提高性能分析的效率;

未勾选 Disable JavaScript samples 的情况下,报告区记录了 JavaScript 执行时的堆栈信息 (图中红线框起的部分)

勾选了 Disable JavaScript samples 的情况下,报告区只记录了一些高级事件 ,比如:Function Call、渲染相关事件等:

Enable advanced paint instrumentation

这个选项用于启用高级绘制仪器;当你勾选了这个选项后,Chrome 将记录更详细的绘制信息,包括每个绘制操作的时间、持续时间、绘制区域等。

勾选了 Enable advanced paint instrumentation 后,当鼠标选中报告中 Frames 数据的某一帧时,在下方的面板里就会多出一个 Layers 的选项;

我们知道,浏览器在渲染一个页面时会有针对性的对一些效果进行分层,Layers 的选项下展示的正是页面的图层树信息

通过观察这些绘制操作的时间和持续时间,就能判断哪些绘制操作占用了较长的时间,可能导致页面渲染缓慢对症下药。

CPU 和 Network

CPU 下拉选项中,我们可以通过降低 CPU 的性能,来模拟低性能设备上的页面运行情况;比如移动设备,或者一些比较老旧的计算机;

Network 下拉框可以选择不同的网络条件,用于模拟网络不佳的情况下页面的运行情况

Hardware concurrency

hardware concurrency 选项用于模拟硬件并发性能;勾选这个选项后,我们就可以选择从 1 到最大硬件并发性能水平之间的值,以模拟不同的硬件环境。

通过切换不同的 hardware concurrency 值,可以观察页面在不同硬件并发性能水平下的性能表现;这对于优化页面的并发处理和多线程任务非常有帮助。

硬件并发性能是指处理器(CPU)同时执行多个线程或任务的能力。现代计算机通常具有多核处理器,每个核心都可以同时执行多个线程。硬件并发性能的提高可以显著提升计算机的性能和响应能力。

报告区

接下来就是我们的重头戏,最终性能分析报告呈现的区域;我们之前介绍的操作、设置最终都是为性能报告服务。

我们以掘金首页为例,来看看报告区都有哪些内容。

首先,我们要打开谷歌浏览器的无痕模式 ;默认快捷键是 Command+Option+N (Mac) 或者 Control+Shift+N (Windows, Linux);

然后打开 掘金首页,点击 (Start Profiling and reload page) 按钮来进行录制;

等待几秒钟后,一份完整的性能报告就呈现出来啦:

从上图中可以看出,这份性能报告大概可以分为两块区域:概览区、性能报告区

而所有的这些性能指标都是基于时间的维度来展示的,因此在性能报告中还有一条贯穿整个报告的时间线

使用无痕模式是为了避免安装的一些插件对性能测试产生影响。

概览区

首先,概览区的 x 轴就是时间线,而 y 轴展示了几个关键指标: CPU 占用情况、NET 网络请求情况、HEAP 堆内存使用量等 ;如果勾选了 Screenshots 还会有每一帧的绘制快照

这些指标会以时间为顺序,通过图表的形式展现出来

当我们用鼠标点击这个区域,在下方的性能报告和详情信息区域,就会呈现这一时间区间的性能情况

同时,还支持通过多种方式改变时间区间的长度:

  • 滚动鼠标滚轮;
  • 按住鼠标左键拖动;
  • 拖拽滑块时间区间两边的滑块;
  • 键盘上的 w 键和 s 键。

而使用键盘上的 a 键和 d 键则可以在固定时间区间长度的同时,精确修改时间区间的作用范围

性能报告区

这个区域呈现了许多性能指标项,我们一起来看看吧。

Network

Network 部分展现的是网络请求的瀑布图,我们点击 图标就可以展开这个面板:

请求的文件类型

从图中可以看出,这些网络请求的瀑布图有许多种颜色,这些颜色就代表了请求对应的文件类型

  • 蓝色: 请求 HTML 文档;
  • 紫色:请求 CSS 文件;
  • 黄色: 请求 JS 文件;
  • 绿色: 请求图片;
请求时长的构成

不知道小伙伴们有没有注意到,瀑布图中的每个请求都被分为了四个部分:左侧的线条、中间浅色的条形图形、中间深色的条形图以及右侧的线条

这些划分又有什么含义呢?

  • 左侧的线:我们知道,在发送一个网络请求前,需要先解析 URL、建立连接等一系列操作;而左侧的线代表了 请求的开始,直到 Connection Start 事件组相关事件结束(包括 Connection Start 事件)
  • 中间的浅色部分条形图代表了 Request Sent 和 Waiting (TTFB,即 Time To First Byte) 的时间;TTFB 是指从发送请求到接收到第一个字节的时间,它表示了服务器响应的延迟时间。
  • 中间的深色部分条形图代表了从接收到第一个字节到请求的所有内容都被下载完成的时间
  • 最后右侧的线代表了主线程等待的时间
请求的优先级

仔细观察每个请求,会发现请求左上角有个小小的方块,这个方块代表了本次请求的优先级。

深蓝色的方块表示本次请求优先级更高;而浅蓝色方块表示优先级较低

请求的详细信息

接下来,我们点击其中一个网络请求,可以看到下方的面板展示了请求的详情信息,包括请求时间、请求方法、优先级等等

Frames

Frames 区域展示了绘制一个帧所花费的确切时间;在这个部分,同样用颜色来区分了四种类型的帧:

  1. 空闲帧(白色):表示在该帧中没有发生任何渲染或更新
  2. 正常帧(绿色 ):表示该帧在适当的时间内被渲染出来,没有出现延迟或问题。
  3. 部分呈现帧(黄色,带有稀疏的宽虚线图案 ):表示 Chrome 尽力在适当的时间内渲染了部分视觉更新。例如:渲染器进程的主线程的工作延迟了,但合成器线程(如滚动)按时完成了渲染,则会出现这种情况。
  4. 丢帧(红色,带有密集的实线图案 ):表示该帧由于性能问题或其他原因导致延迟,无法按时渲染

Timings

Timings 这个模块展示了衡量网页性能(FP、FCP、DCL、LCP、L)的几个关键指标,以及完成指标对应的时间:

我们来看看这些指标都代表什么含义:

FP (First Paint)

指页面的首次渲染的时间点 。在完成首次渲染之前,用户看到的都是 没有任何内容的白色页面 ,也就是我们常说的 白屏时间

FP 可以反映页面加载的整体速度

FCP (First Contentful Paint)

指页面的首个内容绘制时间,即浏览器在页面上绘制出第一块有实际内容的区域(如文本、图像等)的时间点。

FCP 反映了用户可以看到有意义的内容的时间

DCL (DOM Content Loaded)

指当 HTML 文档被完全加载和解析后,DOM 树构建完成并且所有关联资源(如样式表、脚本等)已经下载完成,触发 DOMContentLoaded 事件的时间点

DCL 反映了页面的可交互性,表示页面已经准备好响应用户的操作

LCP (Largest Contentful Paint)

指页面上最大的一个可见元素(如图片、文本等)绘制完成的时间点

LCP 是衡量页面加载速度的重要指标,它反映了页面主要内容的加载完成时间。

L (Load)

指页面完全加载完成的时间点。包括所有资源(如图片、样式表、脚本等)都已下载完成,并且相关的 DOM、CSSOM 和 JavaScript 都已经处理完成。

L 反映了整个页面加载的时间。

Layout Shifts

这个指标用于衡量 页面加载过程中发生的视觉不稳定性

那么什么叫做视觉不稳定性呢?

实际上,当用户在页面加载期间进行交互时,如果页面中的元素发生意外的移动或调整,会导致页面上的元素重新排列,从而造成视觉上的不连续和不稳定感。

这种 元素的移动可能会干扰用户的操作,例如点击了一个不想点到的按钮,或者导致误触其他元素。

我们点击 Layout Shifts 区域的色具体色块,在下方的 Summary 一栏就会展示此次位移的具体信息:

Main

这项指标可谓是整个性能报告中的重点区域,其中展示了 主线程在进行的相关活动

火焰图

Main 指标中图表的这种展现方式称为火焰图;而堆叠组成火焰图的这些彩色矩形,则代表一个个函数调用堆栈:

矩形颜色

Network 用固定颜色来区分请求类型不同,Main 指标中的颜色是随机分配的

但是来自同一个脚本的调用在记录中会被分配为相同的颜色

长任务

小伙伴们可能已经留意到了,在有的 Task 中除了灰色的区域外,还有部分被红色密集实线覆盖,同时右上角还有一个红色的三角 :

这实际上是用来标识这个任务是个长任务

执行时长超过 50 毫秒的任务会被定义为长任务;而超过 50 毫秒的部分就会用这种红色密集实线覆盖

这些长任务可能会阻塞主线程,导致页面卡顿、无法及时响应用户输入等等;是我们需要 重点关注 的对象。

矩形堆叠逻辑

图中位于最上方的 Task 表示 一个由浏览器调度和执行的任务单元Task长度就表示这个任务执行时间的跨度

Task 下方的这些矩形是根据函数之间的调用关系来堆放的,比如 ------

上图中,一个指针事件 pointermove 的触发,导致了一个 (匿名函数调用(Function Call) ;而这个匿名函数调用又引起了 value 函数的调用...以此类推一直到最后的函数 n

任务具体信息

下面我们来查看一下对应任务的具体信息。

当我们选中任意一个任务时,下方的面板就会展现对应的信息:

Summary

Summary 选项卡展示了 当前任务的具体信息,包括长任务警告、总任务时长、各个子任务耗时等等:

Bottom-Up

Bottom-Up 选项卡将当前任务下所有活动都展示出来,并且根据时间进行倒序排序

这样一来,一眼就能看出是哪个活动耗费了较长的时间:

并且还贴心的提供了筛选 (Filter) 功能和分组 (Grouping) 功能,方便我们分类查看:

图中可以看出,整个图表分为三列:

  • Self Time 表示这个根活动自身花费的时间;
  • Total Time 表示这个根活动自身以及导致的子活动花费的时间总和。
  • Activity 就是对应的活动名称;并且点击活动名称右侧的链接,还可以直接跳转到对应的源码。
Call Tree

Call Tree 选项卡可以用来查看当前任务下的根活动,以及根活动引发的一系列子活动的耗时

根活动是那些导致浏览器做一些工作的活动。比如前面提到的 pointermove ,浏览器会触发一个Event活动作为根活动,该事件可能导致处理程序执行,依此类推。

可以简单理解为火焰图顶层的活动。

Event Log

最后的 Event Log 选项卡是按照活动在记录过程中发生的先后顺序来呈现表格的:

这里的下拉不是用来做分组 (Grouping) 的,而是以时间为维度来过滤滤掉耗时少于 1 毫秒或 15 毫秒的活动

而旁边 LoadingScriptingRendingPainting 这些复选框,是用于通过类型来过滤表格中的数据:

并且这里的表格多了一列 Start Time;顾名思义就是对应活动的开始时间。

其余部分

报告区中剩余的信息,比如 ------

  • GPU: 展示了 GPU 的活动情况;
  • Compositor: 展示了合成线程的活动情况;

这些都大同小异,在这里就不一一展开讲啦,留给大家自己去探索。

总结一下

经过上面几万字的输出,想来你已经对 Performance 工具的使用知根知底了;

但是,学会使用它还只是纸上谈兵;更多的是需要我们自己动手去分析实战。

各位小伙伴不妨找几个网站来练练手,相信很快就能熟能生巧啦~

作者:hprep

链接:juejin.cn/post/729408...

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

前端性能优化------首页资源压缩63%、白屏时间缩短86%

提升首屏的加载速度,是前端性能优化中最重要 的环节,这里笔者梳理出一些 常规且有效 的首屏优化建议

目标: 通过对比优化前后的性能变化,来验证方案的有效性,了解并掌握其原理

1、路由懒加载

SPA 项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面打包成一个文件,当用户打开首页时,会一次性加载所有的资源,造成首页加载很慢,降低用户体验

列一个实际项目的打包详情:

  • app.js 初始体积: 1175 KB

  • app.css 初始体积: 274 KB

将路由全部改成懒加载

ini 复制代码
js
 代码解读
复制代码
// 通过webpackChunkName设置分割后代码块的名字
const Home = () => import(/* webpackChunkName: "home" */ "@/views/home/index.vue");
const MetricGroup = () => import(/* webpackChunkName: "metricGroup" */ "@/views/metricGroup/index.vue");
............
const routes = [
    {
       path: "/",
       name: "home",
       component: Home
    },
    {
       path: "/metricGroup",
       name: "metricGroup",
       component: MetricGroup
    },
    ............
 ]

重新打包后,首页资源拆分为 app.js 和 home.js,以及对应的 css 文件

  • app.js:244 KB、 home.js: 35KB

  • app.css:67 KB、home.css: 15KB

通过路由懒加载,该项目的首页资源压缩约 52%

路由懒加载的原理

懒加载前提的实现:ES6的动态地加载模块------import()

调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中 ------摘自《webpack------模块方法》的import()小节

要实现懒加载,就得先将进行懒加载的子模块分离出来,打包成一个单独的文件

webpackChunkName 作用是 webpack 在打包的时候,对异步引入的库代码(lodash)进行代码分割时,设置代码块的名字。webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中

2、组件懒加载

除了路由的懒加载外,组件的懒加载在很多场景下也有重要的作用

举个🌰:

home 页面 和 about 页面,都引入了 dialogInfo 弹框组件,该弹框不是一进入页面就加载,而是需要用户手动触发后才展示出来

home 页面示例:

xml 复制代码
js
 代码解读
复制代码
<template>
  <div class="homeView">
    <p>home 页面</p>
    <el-button @click="dialogVisible = !dialogVisible">打开弹框</el-button>
    <dialogInfo v-if="dialogVisible" />
  </div>
</template>
<script>
import dialogInfo from '@/components/dialogInfo';
export default {
  name: 'homeView',
  components: {
    dialogInfo
  }
}
</script>

项目打包后,发现 home.js 和 about.js 均包括了该弹框组件的代码(在 dist 文件中搜索dialogInfo弹框组件)

当用户打开 home 页时,会一次性加载该页面所有的资源,我们期望的是用户触发按钮后,再加载该弹框组件的资源

这种场景下,就很适合用懒加载的方式引入

弹框组件懒加载:

xml 复制代码
xml
 代码解读
复制代码
<script>
const dialogInfo = () => import(/* webpackChunkName: "dialogInfo" */ '@/components/dialogInfo');
export default {
  name: 'homeView',
  components: {
    dialogInfo
  }
}
</script>

重新打包后,home.js 和 about.js 中没有了弹框组件的代码,该组件被独立打包成 dialogInfo.js,当用户点击按钮时,才会去加载 dialogInfo.js 和 dialogInfo.css

最终,使用组件路由懒后,该项目的首页资源进一步减少约 11%

组件懒加载的使用场景

有时资源拆分的过细也不好,可能会造成浏览器 http 请求的增多

总结出三种适合组件懒加载的场景:

1)该页面的 JS 文件体积大,导致页面打开慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度(比如首页)

2)该组件不是一进入页面就展示,需要一定条件下才触发(比如弹框组件)

3)该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的 JS 文件大小(比如表格组件、图形组件等)

3、合理使用 Tree shaking

Tree shaking 的作用:消除无用的 JS 代码,减少代码体积

举个🌰:

javascript 复制代码
javascript
 代码解读
复制代码
// util.js
export function targetType(target) {
  return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
}
export function deepClone(target) {
  return JSON.parse(JSON.stringify(target));
}

项目中只使用了 targetType 方法,但未使用 deepClone 方法,项目打包后,deepClone 方法不会被打包到项目里

tree-shaking 原理:

依赖于ES6的模块特性,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是 tree-shaking 的基础

静态分析就是不需要执行代码,就可以从字面量上对代码进行分析。ES6之前的模块化,比如 CommonJS 是动态加载,只有执行后才知道引用的什么模块,就不能通过静态分析去做优化,正是基于这个基础上,才使得 tree-shaking 成为可能

Tree shaking 并不是万能的

并不是说所有无用的代码都可以被消除,还是上面的代码,换个写法 tree-shaking 就失效了

javascript 复制代码
js
 代码解读
复制代码
// util.js
export default {
  targetType(target) {
    return Object.prototype.toString.call(target).slice(8, -1).toLowerCase();
  },
  deepClone(target) {
    return JSON.parse(JSON.stringify(target));
  }
};

// 引入并使用
import util from '../util';
util.targetType(null)

同样的,项目中只使用了 targetType 方法,未使用 deepClone 方法,项目打包后,deepClone 方法还是被打包到项目里

在 dist 文件中搜索 deepClone 方法:

究其原因,export default 导出的是一个对象,无法通过静态分析判断出一个对象的哪些变量未被使用,所以 tree-shaking 只对使用 export 导出的变量生效

这也是函数式编程越来越火的原因,因为可以很好利用 tree-shaking 精简项目的体积,也是 vue3 全面拥抱了函数式编程的原因之一

4、骨架屏优化白屏时长

使用骨架屏,可以缩短白屏时间,提升用户体验。国内大多数的主流网站都使用了骨架屏,特别是手机端的项目

SPA 单页应用,无论 vue 还是 react,最初的 html 都是空白的,需要通过加载 JS 将内容挂载到根节点上,这套机制的副作用:会造成长时间的白屏

常见的骨架屏插件就是基于这种原理,在项目打包时将骨架屏的内容直接放到 html 文件的根节点中

使用骨架屏插件,打包后的 html 文件(根节点内部为骨架屏):

同一项目,对比使用骨架屏前后的 FP 白屏时间:

  • 无骨架屏:白屏时间 1063ms
  • 有骨架屏:白屏时间 144ms

骨架屏确实是优化白屏的不二选择,白屏时间缩短了 86%

骨架屏插件

这里以 vue-skeleton-webpack-plugin 插件为例,该插件的亮点是可以给不同的页面设置不同的骨架屏,这点确实很酷

1)安装

css 复制代码
npm i vue-skeleton-webpack-plugin 

2)vue.config.js 配置

javascript 复制代码
// 骨架屏
const SkeletonWebpackPlugin = require("vue-skeleton-webpack-plugin");
module.exports = {
   configureWebpack: {
      plugins: [
       new SkeletonWebpackPlugin({
        // 实例化插件对象
        webpackConfig: {
          entry: {
            app: path.join(__dirname, './src/skeleton.js') // 引入骨架屏入口文件
          }
        },
        minimize: true, // SPA 下是否需要压缩注入 HTML 的 JS 代码
        quiet: true, // 在服务端渲染时是否需要输出信息到控制台
        router: {
          mode: 'hash', // 路由模式
          routes: [
            // 不同页面可以配置不同骨架屏
            // 对应路径所需要的骨架屏组件id,id的定义在入口文件内
            { path: /^/home(?:/)?/i, skeletonId: 'homeSkeleton' },
            { path: /^/detail(?:/)?/i, skeletonId: 'detailSkeleton' }
          ]
        }
      })        
      ]
   }
}

3)新建 skeleton.js 入口文件

arduino 复制代码
// skeleton.js
import Vue from "vue";
// 引入对应的骨架屏页面
import homeSkeleton from "./views/homeSkeleton";
import detailSkeleton from "./views/detailSkeleton";

export default new Vue({
    components: {
        homeSkeleton,
        detailSkeleton,
    },
    template: `
    <div>
      <homeSkeleton id="homeSkeleton" style="display:none;" />
      <detailSkeleton id="detailSkeleton" style="display:none;" />
    </div>
  `,
});

5、长列表虚拟滚动

首页中不乏有需要渲染长列表的场景,当渲染条数过多时,所需要的渲染时间会很长,滚动时还会造成页面卡顿,整体体验非常不好

虚拟滚动------指的是只渲染可视区域的列表项,非可见区域的不渲染,在滚动时动态更新可视区域,该方案在优化大量数据渲染时效果是很明显的

虚拟滚动图例:

虚拟滚动基本原理:

计算出 totalHeight 列表总高度,并在触发时滚动事件时根据 scrollTop 值不断更新 startIndex 以及 endIndex ,以此从列表数据 listData 中截取对应元素

虚拟滚动性能对比:

  • 在不使用虚拟滚动的情况下,渲染10万个文本节点:

  • 使用虚拟滚动的情况后:

使用虚拟滚动使性能提升了 78%

虚拟滚动插件

虚拟滚动的插件有很多,比如 vue-virtual-scroller、vue-virtual-scroll-list、react-tiny-virtual-list、react-virtualized 等

这里简单介绍 vue-virtual-scroller 的使用

arduino 复制代码
// 安装插件
npm install vue-virtual-scroller

// main.js
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

Vue.use(VueVirtualScroller)

// 使用
<template> 
  <RecycleScroller 
    class="scroller" 
    :items="list" 
    :item-size="32" 
    key-field="id" 
    v-slot="{ item }"> 
      <div class="user"> {{ item.name }} </div>
  </RecycleScroller> 
</template>

该插件主要有 RecycleScroller.vue、DynamicScroller.vue 这两个组件,其中 RecycleScroller 需要 item 的高度为静态的,也就是列表每个 item 的高度都是一致的,而 DynamicScroller 可以兼容 item 的高度为动态的情况

6、Web Worker 优化长任务

由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系,当页面中有很多长任务时,会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况

查看页面的长任务:

打开控制台,选择 Performance 工具,点击 Start 按钮,展开 Main 选项,会发现有很多红色的三角,这些就属于长任务(长任务:执行时间超过50ms的任务)

测试实验:

如果直接把下面这段代码直接丢到主线程中,计算过程中页面一直处于卡死状态,无法操作

ini 复制代码
js
 代码解读
复制代码
let sum = 0;
for (let i = 0; i < 200000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random()
    }
  }

使用 Web Worker 执行上述代码时,计算过程中页面正常可操作、无卡顿

ini 复制代码
js
 代码解读
复制代码
// worker.js
onmessage = function (e) {
  // onmessage获取传入的初始值
  let sum = e.data;
  for (let i = 0; i < 200000; i++) {
    for (let i = 0; i < 10000; i++) {
      sum += Math.random()
    }
  }
  // 将计算的结果传递出去
  postMessage(sum);
}

Web Worker 具体的使用与案例,详情见 一文彻底了解Web Worker,十万、百万条数据都是弟弟🔥

Web Worker 的通信时长

并不是执行时间超过 50ms 的任务,就可以使用 Web Worker,还要先考虑通信时长的问题

假如一个运算执行时长为 100ms,但是通信时长为 300ms, 用了 Web Worker可能会更慢

比如新建一个 web worker, 浏览器会加载对应的 worker.js 资源,下图中的 Time 是这个资源的通信时长(也叫加载时长)

当任务的运算时长 - 通信时长 > 50ms,推荐使用Web Worker

7、requestAnimationFrame 制作动画

requestAnimationFrame 是浏览器专门为动画提供的 API,它的刷新频率与显示器的频率保持一致,使用该 api 可以解决用 setTimeout/setInterval 制作动画卡顿的情况

下面的案例演示了两者制作进度条的对比(运行按钮可点击)

可以看到使用定时器制作的动画,卡顿还是比较明显的

setTimeout/setInterval、requestAnimationFrame 三者的区别:

1)引擎层面

setTimeout/setInterval 属于 JS引擎,requestAnimationFrame 属于 GUI引擎

JS引擎与GUI引擎是互斥的,也就是说 GUI 引擎在渲染时会阻塞 JS 引擎的计算

2)时间是否准确

requestAnimationFrame 刷新频率是固定且准确的,但 setTimeout/setInterval 是宏任务,根据事件轮询机制,其他任务会阻塞或延迟js任务的执行,会出现定时器不准的情况

3)性能层面

当页面被隐藏或最小化时,setTimeout/setInterval 定时器仍会在后台执行动画任务,而使用 requestAnimationFrame 当页面处于未激活的状态下,屏幕刷新任务会被系统暂停

8、JS 的6种加载方式

1)正常模式

xml 复制代码
xml
 代码解读
复制代码
<script src="index.js"></script>

这种情况下 JS 会阻塞 dom 渲染,浏览器必须等待 index.js 加载和执行完成后才能去做其它事情

2)async 模式

xml 复制代码
xml
 代码解读
复制代码
<script async src="index.js"></script>

async 模式下,它的加载是异步的,JS 不会阻塞 DOM 的渲染,async 加载是无顺序的,当它加载结束,JS 会立即执行

使用场景:若该 JS 资源与 DOM 元素没有依赖关系,也不会产生其他资源所需要的数据时,可以使用async 模式,比如埋点统计

3)defer 模式

xml 复制代码
xml
 代码解读
复制代码
<script defer src="index.js"></script>

defer 模式下,JS 的加载也是异步的,defer 资源会在 DOMContentLoaded 执行之前,并且 defer 是有顺序的加载

如果有多个设置了 defer 的 script 标签存在,则会按照引入的前后顺序执行,即便是后面的 script 资源先返回

所以 defer 可以用来控制 JS 文件的执行顺序,比如 element-ui.js 和 vue.js,因为 element-ui.js 依赖于 vue,所以必须先引入 vue.js,再引入 element-ui.js

xml 复制代码
xml
 代码解读
复制代码
<script defer src="vue.js"></script>
<script defer src="element-ui.js"></script>

defer 使用场景:一般情况下都可以使用 defer,特别是需要控制资源加载顺序时

4)module 模式

xml 复制代码
xml
 代码解读
复制代码
<script type="module">import { a } from './a.js'</script>

在主流的现代浏览器中,script 标签的属性可以加上 type="module",浏览器会对其内部的 import 引用发起 HTTP 请求,获取模块内容。这时 script 的行为会像是 defer 一样,在后台下载,并且等待 DOM 解析

Vite 就是利用浏览器支持原生的 es module 模块,开发时跳过打包的过程,提升编译效率

5) preload

ini 复制代码
ini
 代码解读
复制代码
<link rel="preload" as="script" href="index.js">

link 标签的 preload 属性:用于提前加载一些需要的依赖,这些资源会优先加载(如下图红框)

vue2 项目打包生成的 index.html 文件,会自动给首页所需要的资源,全部添加 preload,实现关键资源的提前加载

preload 特点:

1)preload 加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞 onload 事件;

2)preload 加载的 JS 脚本其加载和执行的过程是分离的,即 preload 会预加载相应的脚本代码,待到需要时自行调用;

6)prefetch

ini 复制代码
ini
 代码解读
复制代码
<link rel="prefetch" as="script" href="index.js">

prefetch 是利用浏览器的空闲时间,加载页面将来可能用到的资源的一种机制;通常可以用于加载其他页面(非首页)所需要的资源,以便加快后续页面的打开速度

prefetch 特点:

1)pretch 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少5分钟(无论资源是否可以缓存)

2)当页面跳转时,未完成的 prefetch 请求不会被中断

加载方式总结

async、defer 是 script 标签的专属属性,对于网页中的其他资源,可以通过 link 的 preload、prefetch 属性来预加载

如今现代框架已经将 preload、prefetch 添加到打包流程中了,通过灵活的配置,去使用这些预加载功能,同时我们也可以审时度势地向 script 标签添加 async、defer 属性去处理资源,这样可以显著提升性能

9、图片的优化

平常大部分性能优化工作都集中在 JS 方面,但图片也是页面上非常重要的部分

特别是对于移动端来说,完全没有必要去加载原图,浪费带宽。如何去压缩图片,让图片更快的展示出来,有很多优化工作可以做

淘宝首页的图片资源都很小:

图片的动态裁剪

很多云服务,比如阿里云七牛云,都提供了图片的动态裁剪功能,效果很棒,确实是钱没有白花

只需在图片的url地址上动态添加参数,就可以得到你所需要的尺寸大小,比如:http://7xkv1q.com1.z0.glb.clouddn.com/grape.jpg?imageView2/1/w/200/h/200

图片瘦身前后对比:

  • 原图:1.8M
  • 裁剪后:12.8KB

经过动态裁剪后的图片,加载速度会有非常明显的提升

图片的懒加载

对于一些图片量比较大的首页,用户打开页面后,只需要呈现出在屏幕可视区域内的图片,当用户滑动页面时,再去加载出现在屏幕内的图片,以优化图片的加载效果

图片懒加载实现原理:

由于浏览器会自动对页面中的 img 标签的 src 属性发送请求并下载图片,可以通过 html5 自定义属性 data-xxx 先暂存 src 的值,然后在图片出现在屏幕可视区域的时候,再将 data-xxx 的值重新赋值到 img 的 src 属性即可

ini 复制代码
ini
 代码解读
复制代码
<img src="" alt="" data-src="./images/1.jpg">
<img src="" alt="" data-src="./images/2.jpg">

这里以 vue-lazyload 插件为例

javascript 复制代码
javascript
 代码解读
复制代码
// 安装 
npm install vue-lazyload 
    
// main.js 注册
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 配置项
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: 'dist/error.png', // 图片加载失败时的占位图
  loading: 'dist/loading.gif', // 图片加载中时的占位图
  attempt: 1
})

// 通过 v-lazy 指令使用
<ul>  
    <li v-for="img in list">
        <img v-lazy="img.src" :key="img.src" >
    </li>
</ul>

使用字体图标

字体图标是页面使用小图标的不二选择,最常用的就是 iconfont

字体图标的优点:

1)轻量级:一个图标字体要比一系列的图像要小。一旦字体加载了,图标就会马上渲染出来,减少了 http 请求

2)灵活性:可以随意的改变颜色、产生阴影、透明效果、旋转等

3)兼容性:几乎支持所有的浏览器,请放心使用

图片转 base64 格式

将小图片转换为 base64 编码字符串,并写入 HTML 或者 CSS 中,减少 http 请求

转 base64 格式的优缺点:

1)它处理的往往是非常小的图片,因为 Base64 编码后,图片大小会膨胀为原文件的 4/3,如果对大图也使用 Base64 编码,后者的体积会明显增加,即便减少了 http 请求,也无法弥补这庞大的体积带来的性能开销,得不偿失

2)在传输非常小的图片的时候,Base64 带来的文件体积膨胀、以及浏览器解析 Base64 的时间开销,与它节省掉的 http 请求开销相比,可以忽略不计,这时候才能真正体现出它在性能方面的优势

项目可以使用 url-loader 将图片转 base64:

css 复制代码
css
 代码解读
复制代码
// 安装
npm install url-loader --save-dev
    
// 配置
module.exports = {
  module: {
    rules: [{
        test: /.(png|jpg|gif)$/i,
        use: [{
            loader: 'url-loader',
            options: {
              // 小于 10kb 的图片转化为 base64
              limit: 1024 * 10
            }
        }]
     }]
  }
};

优化总结

本文主要介绍的是 代码层面 的性能优化,经过上面的一系列优化,首页打开速度有了明显的提升,虽然都是一些常规方案,但其中可以深挖的知识点并不少

下一篇文章: 前端内存优化知多少?内存泄露只是冰山一角,聊一聊前端内存方面的优化,小伙们一起来看看其中又有哪些知识点

参考文章:
路由懒加载原理及使用
vue-skeleton-webpack-plugin 骨架屏插件使用
前端性能优化-虚拟滚动
requestAnimationFrame制作动画
浅谈script标签中的async和defer
Tree-Shaking性能优化实践 - 原理篇
使用 Preload&Prefetch 优化前端页面的资源加载

相关推荐
国科安芯2 小时前
【AS32系列MCU调试教程】性能优化:Eclipse环境下AS32芯片调试效率提升
java·性能优化·eclipse
雨果talk4 小时前
Spring Boot集成Mina的Socket资源管理:从稳定通信到高性能优化
spring boot·后端·性能优化
William Dawson11 小时前
【React Native 性能优化:虚拟列表嵌套 ScrollView 问题全解析】
react native·react.js·性能优化
EndingCoder11 小时前
React Native 性能优化实践
react native·react.js·性能优化
ZFJ_张福杰13 小时前
【Flutter】性能优化总结
flutter·性能优化
大熊猫侯佩13 小时前
Swift 初学者交心:在 Array 和 Set 之间我们该如何抉择?
数据结构·性能优化·swift
零叹1 天前
篇章五 系统性能优化——资源优化——CPU优化(2)
性能优化·对象池·锁优化·java高并发编程·并发数据结构·缓存更新
异常君1 天前
Redis String 类型的底层实现与性能优化
java·redis·性能优化
异常君1 天前
用户态与内核态:Java 程序员必懂的两种执行状态
性能优化·操作系统·cpu