性能优化
防抖/节流、渲染与内存、资源预加载及引擎行为是前端性能的主要抓手。相关 CSS 见 视觉效果、Head 资源提示。
一、代码层面优化
1. 防抖(Debounce)与节流(Throttle)实现
防抖 – 延迟执行,连续触发只执行最后一次
应用场景:搜索输入、窗口resize、表单验证。
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 立即执行版(首次立即执行,后续防抖)
function debounceImmediate(fn, delay) {
let timer = null;
return function (...args) {
if (timer === null) fn.apply(this, args);
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
}, delay);
};
}
节流 – 固定频率执行
应用场景:滚动事件、鼠标移动、射击游戏。
// 时间戳版(立即执行,停止后不执行最后一次)
function throttle(fn, interval) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
fn.apply(this, args);
last = now;
}
};
}
// 定时器版(延时执行,停止后会再执行一次)
function throttleTimer(fn, interval) {
let timer = null;
return function (...args) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, interval);
};
}
// 融合版:保证结束时还执行一次(常用)
function throttleMerge(fn, interval) {
let last = 0,
timer = null;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - last);
if (remaining <= 0) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
last = now;
} else if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
last = Date.now();
timer = null;
}, remaining);
}
};
}
2. 避免内存泄漏
| 常见泄漏源 | 示例 | 解决方案 |
|---|---|---|
| 意外的全局变量 | function foo() { a = 1; } | 使用 'use strict',始终声明变量 let/const/var |
| 未清理的定时器 | setInterval(() => {...}, 1000) 未清除 | 在组件卸载或不需要时 clearInterval/clearTimeout |
| 未移除的事件监听 | window.addEventListener('resize', handler) | 提供对应的 removeEventListener,尤其在 SPA 路由切换时 |
| 闭包引用过大 | 闭包持有大数组或 DOM 元素 | 将不需要的变量设为 null,避免长期持有 |
| DOM 引用未清 | 在 JS 中保存已删除 DOM 节点的引用 | 删除节点前先置引用为 null |
| 脱离 DOM 树但被 JS 引用 | let elem = document.getElementById('btn'); elem.remove(); 但变量仍存在 | 主动 elem = null |
| Map/Set 未清除 | 使用普通 Map 存储 DOM 节点,节点删除后 Map 仍持有 | 使用 WeakMap / WeakSet 自动弱引用 |
3. 重排(Reflow)与重绘(Repaint)的触发与优化
- 重排(reflow):布局几何属性变化(宽高、边距、位置、字体、添加/删除 DOM、窗口 resize、获取
offsetWidth等)→ 重新计算布局树。 - 重绘(repaint):视觉样式变化(背景色、颜色、可见性)但不影响布局 → 仅重新绘制。
优化原则:
- 避免逐条修改样式,使用
class或cssText。 - 使元素脱离文档流(
position: absolute/fixed,display: none)进行批量 DOM 操作。 - 避免频繁读取布局属性(如
offsetTop),改用变量缓存。 - 复杂动画使用
transform、opacity(只触发合成,不触发重排/重绘)。 - 使用
documentFragment批量更新 DOM。 - 将动画效果应用到
position: fixed/absolute元素上。
// 坏
for (let i = 0; i < 1000; i++) {
element.style.left = i + "px"; // 每次触发重排
}
// 好 - 使用 transform
element.style.transform = `translateX(1000px)`;
4. 函数节流/防抖 + requestAnimationFrame 动画最佳实践
- 滚动事件:用
requestAnimationFrame合并样式修改,避免过度触发。
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
// 更新样式或处理
ticking = false;
});
ticking = true;
}
});
- 动画循环:始终使用
requestAnimationFrame而非setInterval,它匹配屏幕刷新率,且在页面不可见时停止,节省资源。 - 配合节流:限制高频率事件(如
mousemove)的回调中执行复杂操作,但动画更新本身仍用 rAF。
let lastTimestamp = 0;
function onMouseMove(e) {
throttle(() => {
requestAnimationFrame(() => updatePosition(e.clientX, e.clientY));
}, 16);
}
二、网络优化
1. 资源预加载指令
| 指令 | 作用 | 典型场景 |
|---|---|---|
<link rel="preload" href="..." as="..."> | 提前加载当前页面必需资源(高优先级) | 字体、关键 CSS/JS、大图 |
<link rel="prefetch" href="..."> | 预取下一页资源(低优先级,空闲时加载) | 路由懒加载的下一屏模块 |
<link rel="preconnect" href="https://..."> | 提前建立 TCP+TLS 连接 | 跨域 API 域名、CDN |
<link rel="dns-prefetch" href="..."> | 提前做 DNS 解析 | 第三方资源域名 |
注意:preload 要配合 as 属性正确设置,否则可能二次加载。
2. HTTP/2 多路复用与压缩
- HTTP/2 多路复用:单一连接上并发传输多个请求和响应,消除队头阻塞(应用层)。提升复用 TCP 连接效率,配合服务器推送可主动发送资源。
- 压缩算法:
- Gzip:广泛支持,压缩比中等。
- Brotli:压缩比更高,尤其文本资源(JS/CSS/HTML),比 gzip 小 20%~30%。现代浏览器和服务器(Nginx、CDN)均支持。
- 启用方法:
Content-Encoding: br或gzip。
3. 图片懒加载与响应式图片
懒加载
- 使用
IntersectionObserver监听图片进入视口,替换src。 - 原生懒加载:
<img loading="lazy">(兼容性好,但精细控制可用 JS)。
const imgObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
imgObserver.unobserve(img);
}
});
});
document.querySelectorAll("img[data-src]").forEach((img) => imgObserver.observe(img));
响应式图片
srcset+sizes:根据屏幕宽度选择不同尺寸。
<img
srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 480px, (max-width: 1000px) 800px, 1200px"
src="fallback.jpg"
alt="..."
/>
<picture>元素:根据媒体查询或格式支持选择(如 WebP/AVIF)。
<picture>
<source srcset="image.avif" type="image/avif" />
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="" />
</picture>
三、运行时优化:V8 引擎原理
V8 是 Chrome 和 Node.js 中的 JavaScript 引擎,理解其内部机制有助于写出高性能代码。
1. 隐式类型转换
在需要类型时,JS 会自动转换,但会降低性能并可能导致意外。
// 坏:触发类型转换,且可能无法优化
const total = 100 + "100"; // "100100"
// 好:保持类型一致
const total = 100 + Number("100");
优化建议:
- 使用
===而非==(避免类型猜测)。 - 避免在热函数中改变变量类型(会改变隐藏类)。
2. 隐藏类(Hidden Classes)
V8 将对象结构(属性名、顺序)编码为隐藏类。如果两个对象具有相同的属性名和顺序,它们共享同一个隐藏类,从而加速属性访问。
导致隐藏类退化的操作:
- 动态添加/删除属性。
- 属性赋值顺序不同。
// 相同隐藏类
const p1 = { name: "A", age: 20 };
const p2 = { name: "B", age: 22 };
// 不同隐藏类(差顺序)
const p3 = { age: 20, name: "C" };
优化:在构造函数或对象字面量中一次性定义所有属性,不要动态添加。
3. 去优化(Deoptimization)
V8 在运行时进行推测优化(假设代码模式不变)。当假设被打破时,会回退到非优化代码,称为去优化。
常见触发去优化的操作:
- 函数中传入不同类型参数(例如有时是
number,有时是string)。 - 在
try/catch中的热代码路径。 - 使用
debugger、with、eval。
优化建议:
- 保持函数参数类型稳定。
- 避免在高频率函数(如动画循环)中使用
try/catch,可以将错误处理放到外层。 - 切勿在生产代码中使用
eval或with。
总结
| 类别 | 关键技术点 |
|---|---|
| 防抖节流 | 防抖合并连续触发,节流限制频率;实现中注意 this 和参数传递、立即执行/尾部执行变体 |
| 内存泄漏 | 警惕全局变量、定时器、事件监听、闭包、DOM 引用;使用 WeakMap/WeakSet 辅助 |
| 渲染优化 | 减少重排重绘,利用 transform/opacity,批量 DOM 操作,requestAnimationFrame 合并更新 |
| 网络预加载 | preload(关键)、prefetch(下页)、preconnect(连接预热)、dns-prefetch |
| HTTP 优化 | HTTP/2 多路复用 + Brotli/gzip 压缩 |
| 图片优化 | 懒加载(IntersectionObserver / loading="lazy")、srcset + sizes、picture + WebP/AVIF |
| V8 优化 | 保持类型稳定(避免隐式转换)、使用一致的隐藏类(完整定义对象属性)、避免去优化(稳定参数) |
结合 Lighthouse、Performance 面板与内存快照做针对性优化。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| MDN:requestAnimationFrame | 动画帧 |
| MDN:rel=preload | 预加载 |
| web.dev:Performance | 性能专题(英文) |
| web.dev:Optimize LCP | 加载性能(英文) |
相关文章
错误处理与调试
结构化错误类型、try/catch 与全局未捕获处理是健壮应用的基础。
浏览器相关 API
DOM 事件、存储、fetch、Worker、Observer 等由浏览器提供的 Web API(非 ECMAScript 核心)。HTML 结构见 标签。
模块化与工程化
ESM、CommonJS 与打包工具(Tree Shaking、代码分割)是前端工程化的基础。入门见 JavaScript 基础 · 模块。
执行模型与作用域
执行上下文与词法作用域决定变量在何处可访问、闭包如何形成,以及 var / let / const 的差异。语言概览见 JavaScript 基础。
数据结构与集合增强
Map / Set、WeakMap / WeakSet、数组高阶方法、TypedArray 与 Symbol 等扩展了内置数据结构能力。见 语言基础。
JavaScript 基础
JavaScript(ECMAScript)是 Web 页面与 Node.js 等运行时的脚本语言:在浏览器中操作 DOM、发起网络请求;在服务端处理 I/O。语法与 HTML、CSS 配合构成前端技术栈。