一、代码执行效率优化
JavaScript 单线程特性决定了长时间运行的同步代码会阻塞主线程,导致页面卡顿。核心思路是减少不必要的计算、避免重复执行。
1. 循环与迭代优化
循环是性能敏感区,尤其是处理大量数据时。
- 减少循环内操作:将循环外可预计算的值、DOM 查询、函数调用移到循环外。
- 使用高效迭代方式:
for循环性能优于forEach/map(尤其数据量大时),for...of在现代引擎中性能接近for。
javascript
// 优化前:循环内重复查询DOM和计算
const list = document.querySelectorAll('.list-item');
for (let i = 0; i < list.length; i++) { // 每次循环都查询list.length
list[i].style.width = (i * 10 + 5) + 'px'; // 重复计算
}
// 优化后:缓存长度、预计算
const list = document.querySelectorAll('.list-item');
const len = list.length; // 缓存长度
let width;
for (let i = 0; i < len; i++) {
width = i * 10 + 5; // 单次计算
list[i].style.width = width + 'px';
}
2. 函数优化:防抖与节流
高频触发的事件(如scroll、resize、输入框搜索)会导致函数频繁执行,浪费资源。
- 防抖(Debounce):触发后延迟 n 秒执行,若 n 秒内再次触发则重新计时(适合搜索输入、提交按钮防重复点击)。
- 节流(Throttle):每隔 n 秒最多执行一次(适合滚动加载、鼠标移动跟踪)。
javascript
// 防抖实现
function debounce(fn, delay = 300) {
let timer = null;
return (...args) => {
clearTimeout(timer); // 清除上次定时器
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流实现(时间戳版)
function throttle(fn, interval = 300) {
let lastTime = 0;
return (...args) => {
const now = Date.now();
if (now - lastTime >= interval) { // 间隔达标才执行
fn.apply(this, args);
lastTime = now;
}
};
}
// 应用:监听滚动事件
window.addEventListener('scroll', throttle(() => {
console.log('滚动位置:', window.scrollY);
}, 200)); // 每200ms最多执行一次
二、内存管理与泄漏优化
JavaScript 虽有自动垃圾回收(GC),但不合理的引用会导致内存泄漏(内存占用持续上升,最终引发页面卡顿或崩溃)。
1. 常见内存泄漏场景及解决
-
意外全局变量:未声明的变量会挂载到
window,成为常驻内存的全局变量。javascript
// 优化前:意外全局变量(未用let/const声明) function handleData() { data = { /* 大量数据 */ }; // 等价于window.data } // 优化后:使用局部变量,函数执行完自动回收 function handleData() { const data = { /* 大量数据 */ }; // 局部变量,无引用时被GC回收 } -
未移除的事件监听器:DOM 元素被移除后,若事件监听器未解绑,会导致元素无法被 GC 回收。
javascript
// 优化前:未移除事件监听 const btn = document.getElementById('btn'); btn.addEventListener('click', handleClick); // 后续btn被移除(如通过innerHTML清空父元素),但handleClick仍被引用 // 优化后:主动移除监听 const btn = document.getElementById('btn'); function handleClick() { /* ... */ } btn.addEventListener('click', handleClick); // 当btn即将被移除时 btn.removeEventListener('click', handleClick); -
闭包导致的内存驻留:闭包会保留对外部变量的引用,若闭包长期存在(如挂载到全局),可能导致变量无法回收。
javascript
// 优化前:闭包长期引用大对象 let bigData; function createHandler() { bigData = { /* 10MB数据 */ }; // 被闭包引用 return () => console.log(bigData); } window.handler = createHandler(); // 全局引用,bigData无法回收 // 优化后:按需释放或避免闭包持有大对象 function createHandler() { const bigData = { /* 10MB数据 */ }; return (() => { const dataRef = bigData; // 临时引用 return () => console.log(dataRef); })(); } window.handler = createHandler(); // 执行后bigData无引用,可回收
2. 内存优化工具
- Chrome DevTools Memory 面板:通过 “Take heap snapshot” 对比内存快照,查找泄漏的对象(如 detached DOM 节点、异常增长的数组)。
- Performance 面板:录制操作过程,观察内存曲线是否持续上升(正常应波动稳定)。
三、DOM 操作优化
DOM 操作是性能瓶颈重灾区,因为每次 DOM 修改可能触发重排(Reflow) 或重绘(Repaint):
- 重排:DOM 几何属性变化(如宽高、位置),需重新计算布局,成本高。
- 重绘:DOM 样式变化(如颜色、背景),无需重新布局,成本较低。
核心策略:减少 DOM 操作次数,批量处理,避免频繁触发重排。
1. 批量 DOM 修改
- 使用 DocumentFragment:临时容器,批量添加节点后一次性插入 DOM。
- 离线操作 DOM:先将元素从 DOM 树移除,修改后再插入。
javascript
// 优化前:循环插入DOM,触发多次重排
const list = document.getElementById('list');
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); // 每次appendChild都触发重排
}
// 优化后:DocumentFragment批量处理
const list = document.getElementById('list');
const fragment = document.createDocumentFragment(); // 临时容器
for (let i = 0; i < 100; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item); // 操作离线节点,无重排
}
list.appendChild(fragment); // 一次性插入,仅1次重排
2. 减少重排范围
- 合并样式修改:避免逐条修改样式,改用
class或cssText批量设置。 - 使用 CSS containment:告知浏览器元素独立渲染,限制重排范围。
css
/* 限制重排范围:元素修改不影响其他区域 */
.isolated-element {
contain: layout paint size; /* 布局、绘制、尺寸均独立 */
}
javascript
// 优化前:逐条修改样式,触发多次重排
const el = document.getElementById('box');
el.style.width = '100px';
el.style.height = '200px';
el.style.margin = '10px'; // 3次重排
// 优化后:合并修改,1次重排
const el = document.getElementById('box');
el.style.cssText = 'width: 100px; height: 200px; margin: 10px;';
四、资源加载优化
初始加载速度直接影响用户留存,需减少加载时间和阻塞。
1. 代码分割(Code Splitting)
将代码按路由或组件拆分,按需加载(避免一次性加载全部代码)。
- 配合 ES 模块的
import()动态加载,webpack 等构建工具会自动拆分 chunk。
javascript
// 优化前:一次性加载所有路由组件
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact'; // 即使未访问,也会加载
// 优化后:路由懒加载(仅访问时加载)
const Home = () => import('./pages/Home');
const About = () => import('./pages/About');
const Contact = () => import('./pages/Contact');
// 路由配置(以React Router为例)
<Route path="/about" component={About} /> // 访问/about时才加载About组件代码
2. 减少阻塞资源
- 非关键 JS 延迟加载:对不影响首屏的脚本,添加
async或defer属性。async:下载完成后立即执行(顺序不确定)。defer:下载完成后,等待 HTML 解析完再按顺序执行。
- 关键 CSS 内联:首屏必要样式内联到
<style>,避免外部 CSS 阻塞渲染。
html
<!-- 非关键脚本延迟加载 -->
<script src="analytics.js" async></script> <!-- 统计脚本,不影响首屏 -->
<script src="chart.js" defer></script> <!-- 图表脚本,依赖HTML解析 -->
<!-- 关键CSS内联 -->
<style>
/* 首屏必要样式:如导航、Hero区域 */
.header { height: 60px; }
.hero { min-height: 300px; }
</style>
<!-- 非关键CSS异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'">
五、进阶优化:利用现代 API
-
Web Workers:将计算密集型任务(如大数据处理、复杂算法)移到后台线程,避免阻塞主线程。
javascript
// 主线程:创建Worker并发送数据 const dataWorker = new Worker('data-processor.js'); dataWorker.postMessage(largeDataset); // 发送大量数据 dataWorker.onmessage = (e) => { console.log('处理结果:', e.data); // 接收处理后的数据 }; // data-processor.js(Worker线程) self.onmessage = (e) => { const result = processLargeData(e.data); // 耗时计算 self.postMessage(result); // 发送结果回主线程 }; -
使用高效数据结构:
Map/Set的查找、插入性能优于普通对象(尤其键为字符串 / 数字时)。javascript
// 数据量大时,Map查找性能 > 对象 const map = new Map(); // 插入10万条数据 for (let i = 0; i < 100000; i++) { map.set(`key${i}`, i); } // 查找速度比对象快约20%-50%(视引擎而定) map.get('key50000');
六、性能监测与分析
优化的前提是找到瓶颈,推荐工具:
- Chrome DevTools Performance:录制操作流程,分析主线程阻塞、重排耗时、函数执行时间。
- Lighthouse:生成性能评分(First Contentful Paint、Time to Interactive 等指标),并给出优化建议。
- Core Web Vitals:关注 LCP(最大内容绘制)、FID(首次输入延迟)、CLS(累积布局偏移)三大核心指标,直接关联用户体验。
总结
JavaScript 性能优化的核心原则:先测量,再优化。优先解决用户可感知的问题(如交互卡顿、加载缓慢),避免过度优化(如为微小性能提升牺牲代码可读性)。结合具体场景选择合适策略,才能在实战中高效提升性能。
1327

被折叠的 条评论
为什么被折叠?



