性能优化
推荐阅读:更系统的长文《前端性能优化实战与度量指南》:见本目录
article.md
(配套示例1.html
)。
重绘重排
- 重绘
- 当元素样式发生变化但不影响布局时,浏览器重新绘制元素的过程,比如改变颜色和背景但是不改变几何属性
- 重排
- DOM 元素的尺寸位置发生变化时,浏览器要重新计算布局,影响其他元素位置的过程。 重排一定会触发重绘,重绘不一定触发重排。
DEMO1 批量更改 DOM
/* 问题代码:逐行修改样式会触发多次重排/重绘 */
const el = document.getElementById("el");
// 每行都可能触发重排+重绘
// width、height、margin 都会改变元素几何属性,导致重排
el.style.width = "100px"; // 触发重排+重绘
el.style.height = "100px"; // 触发重排+重绘
el.style.margin = "10px"; // 触发重排+重绘
/* 优化方案1:使用类名一次性应用所有样式 */
// 只会触发一次重排+重绘
el.className = "el"; // CSS类中定义了所有样式
/* 优化方案2:使用cssText合并样式更改 */
// 只会触发一次重排+重绘
el.style.cssText = "width: 100px; height: 100px; margin: 10px;";
/* 优化方案3:使用requestAnimationFrame批量处理 */
requestAnimationFrame(() => {
el.style.width = "100px";
el.style.height = "100px";
el.style.margin = "10px";
}); // 浏览器会在下一帧统一处理样式变更,只触发一次重排+重绘
其他优化方法
- 使用文档片段:
// 使用DocumentFragment减少DOM操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
const el = document.createElement("div");
el.textContent = `Item ${i}`;
fragment.appendChild(el);
}
// 只触发一次重排+重绘
document.body.appendChild(fragment);
- 脱离文档流进行操作:
// 操作前脱离文档流
const el = document.getElementById("el");
const originalDisplay = el.style.display;
const originalPosition = el.style.position;
el.style.position = "absolute";
el.style.display = "none";
// 脱离文档流,不会触发重排
// 多次修改DOM
el.style.width = "100px";
el.style.height = "100px";
el.style.margin = "10px";
// 恢复显示,只触发一次重排+重绘
el.style.display = originalDisplay;
el.style.position = originalPosition;
- 避免强制同步布局:
// 不好的做法 - 强制同步布局
const box = document.getElementById("box");
box.style.width = "100px"; // 修改样式
console.log(box.offsetWidth); // 立即读取布局信息,触发强制同步布局
// 好的做法
const box = document.getElementById("box");
console.log(box.offsetWidth); // 先读取
box.style.width = "100px"; // 后修改
DEMO2 批量更改样式
- 批量更改样式使用 fragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const el = document.createElement("div");
el.textContent = `Item ${i}`;
fragment.appendChild(el);
}
document.body.appendChild(fragment);
// 批量添加元素时,使用document.createDocumentFragment() 创建一个文档片段,然后使用 appendChild() 方法将元素添加到文档片段中,最后使用 appendChild() 方法将文档片段添加到 DOM 中。
更多优化方法
- 使用 CSS3 硬件加速:
// 不好的做法 - 使用left/top进行动画
element.style.left = "100px"; // 触发重排+重绘
// 好的做法 - 使用transform代替
element.style.transform = "translateX(100px)"; // 只触发重绘,GPU加速
- 防抖和节流:
// 滚动事件节流 - 防止过于频繁触发
let ticking = false;
window.addEventListener("scroll", function () {
if (!ticking) {
window.requestAnimationFrame(function () {
// 处理滚动事件的代码
updateElements();
ticking = false;
});
}
ticking = true;
});
- 使用 will-change 提前告知浏览器:
/* 告诉浏览器该元素的transform属性即将发生变化 */
.animated-element {
will-change: transform;
}
- 减少 DOM 深度:
<!-- 减少DOM深度 - 扁平的DOM结构减少重排范围 -->
<div>
<span>Item 1</span>
<span>Item 2</span>
</div>
<!-- 而不是 -->
<div>
<div>
<div>
<span>Item 1</span>
</div>
</div>
<div>
<div>
<span>Item 2</span>
</div>
</div>
</div>
- 分离读写操作:
// 不好的做法 - 交错读写导致多次重排
const width = element.offsetWidth; // 读取
element.style.width = width + 10 + "px"; // 写入
const height = element.offsetHeight; // 读取
element.style.height = height + 10 + "px"; // 写入
// 好的做法 - 先读后写
const width = element.offsetWidth; // 读取
const height = element.offsetHeight; // 读取
element.style.width = width + 10 + "px"; // 写入
element.style.height = height + 10 + "px"; // 写入
- 使用 contain 属性隔离影响范围:
/* 告诉浏览器这个元素的内部变化不会影响外部布局 */
.independent-element {
contain: layout paint;
}
- 虚拟滚动:
// 只渲染可视区域内的元素
function renderVisibleItems() {
const scrollTop = container.scrollTop;
const visibleHeight = container.clientHeight;
// 计算可见范围内的元素索引
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.ceil((scrollTop + visibleHeight) / itemHeight);
// 只渲染可见范围内的元素
for (let i = startIndex; i <= endIndex; i++) {
if (i >= 0 && i < totalItems) {
// 渲染第i个元素
}
}
}
- 使用 缓存布局信息
// offsetTop 读取 ,但是每次读都会重新计算属性触发重排以获得盒子的布局信息
// 强制浏览器计算最新的布局信息,触发重排
for (let i = 0; i < 1000; i++) {
el.style.top = el.offsetTop + i;
}
// 优化做法
let top = el.offsetTop;
for (let i = 0; i < 100; i++) {
top += i;
}
el.style.top = top;
- 使用 transfrom 来代替位置调整
// 触发重排 --> 重绘
el.style.top = el.offsetTop + "px";
// 只触发一次重绘
el.style.transform = `translateY(${el.offsetTop}px)`;
资源加载优化
- 图片懒加载
- 路由懒加载
- 代码分割(code splitting)
- 资源预加载
<link rel="prefetch" href="xxx.js">
预先加载未来可能用到的资源<link rel="preload" href="xxx.js">
高优先级加载关键资源- script 资源加载
- 默认同步执行
defer
延迟执行:HTML 解析完后、DOMContentLoaded
前执行,适合有依赖的脚本async
并发加载、就绪即执行:执行顺序不固定,适合独立脚本(广告/分析)type="module"
使用 ES 模块- webp 格式图片
- 图片优化,减少体积,并质量不受影响
- 图标字体/雪碧图减少 HTTP 请求数
JS 执行优化
- 防抖节流
- webWorkers 处理复杂计算
- requestAnimationFrame 优化动画
- requestIdleCallback 空闲时处理非关键任务
- 调度机制
框架层优化
- memo useMemo useCallback 避免不必要的渲染
- shadcn-ui 按需加载
- 合理使用 key 优化列表渲染
网络层的缓存
强缓存和协商缓存
-
强缓存 Expires/Cache-Control 不发请求
-
协商缓存 其中有两组请求头和响应头:
- Last-Modified/If-Modified-Since 时间戳
- ETag/If-None-Match
-
localStorage/SessionStorage 缓存/Cookie
-
PWA
- 离线缓存
-
网络优化
- CDN 加速
- 存储静态资源,分流 一些数据要走数据库,一些是静态的图片,js,css
- 多路复用 多域名服务器 img1.baidu.com img.baidu.com
- gzip 压缩静态资源
- HTTP/2 多路复用
- DNS 预解析
- CDN 加速
-
首屏优化
- SSR
- 组件渲染在服务器完成,浏览器端直接展示 HTML,再水合
- 骨架屏
- http2.0 serverPush 首屏数据推送,请求了 index.html 直接把相关的 css js 也推送过来
Web Vitals
这张图里展示的是 Web Vitals 核心指标,主要用来衡量网页的用户体验性能:
1. LCP (Largest Contentful Paint)
- 含义:最大内容绘制时间。指页面中 最大可见内容元素(比如大图片、大文字块)出现在屏幕上的时间。
- 目标:≤ 2.5 秒(优秀)。
- 优化方向:减少图片体积、开启懒加载、使用 CDN、优化关键渲染路径(如减少阻塞 JS/CSS)。
2. INP (Interaction to Next Paint)
- 含义:交互到下一次渲染的延迟。衡量用户点击、输入等交互后,浏览器多久给出 视觉反馈。
- 目标:≤ 200ms(优秀)。
3. CLS (Cumulative Layout Shift)
- 含义:累积布局偏移。衡量页面在加载过程中元素 意外跳动 的情况。
- 目标:≤ 0.1(优秀)。
- 优化方向:给图片/视频预留尺寸、避免动态插入 DOM、使用稳定字体加载策略。
👉 提示:在实际项目中建议采集真实用户数据(RUM),并结合实验室工具(Lighthouse/Performance)定位瓶颈。
性能测试
-
Chrome 的 Performance 面板:查看各项性能指标并给出优化建议
-
减少首屏 js/css 体积(code splitting)
-
使用 transform 代替位置调整;预加载关键资源
-
代码拆分示例:将
vue
、vue-router
与业务代码(如App.vue
、Home.vue
、components
)分包,框架包更稳定,便于长期缓存 -
lighthouse
- 测试页面性能
- 是 chrome 的一款性能打分,会在性能 无障碍 最佳实践 SEO 打分 并给出问题和优化建议 细致到每一个方面
- 图片格式大小优化
- 字体库优化
- 渲染阻塞请求
性能的关键指标
- FCP
- First Content Paint, 首次内容绘制,表示浏览器首次渲染出页面内容如文本图片等的时间。
- LCP
- largest contentful paint,表示浏览器首次渲染出页面中最大的内容,如图片,视频等。