当前位置: 首页 > news >正文

JavaScript 性能优化实战:从原理到落地

目录

    • 前言
    • 一、优化原则与核心指标:找准优化方向
      • 1.1 关键性能指标(Web Vitals)
      • 1.2 优化目标:三大核心维度
    • 二、代码层面优化:从 “写好” 到 “写快”
      • 2.1 减少全局变量,避免内存泄漏
      • 2.3 避免频繁 DOM 操作:用 DocumentFragment
      • 2.4 节流(throttle)与防抖(debounce):控制高频函数
    • 三、加载与执行优化:让资源 “快” 起来
      • 3.1 异步加载脚本:async 与 defer
      • 3.2 按需加载模块:动态 import ()
      • 3.3 代码拆分:Webpack/Vite 配置
    • 四、数据与算法优化:提升执行效率
      • 4.1 选择合适的数据结构:Map 替代对象
      • 4.2 优化循环逻辑:减少嵌套与冗余
      • 4.3 Web Workers: offload 密集型计算
        • 1.Worker 脚本(compute.worker.js):处理密集计算
    • 五、浏览器渲染优化:避免 “卡顿” 与 “跳动”
      • 5.1 减少重绘与回流:用 transform 替代 top/left
      • 5.2 requestAnimationFrame:优化动画时序
      • 5.3 启用 GPU 加速:will-change 属性
    • 六、工具与监控:定位性能瓶颈
      • 6.1 Chrome DevTools:前端性能分析利器
      • 6.2 内存泄漏检测:Heap Snapshots
      • 6.3 持续监控:Performance API
    • 七、实战案例:从理论到落地
      • 7.1 列表渲染优化:虚拟滚动
      • 7.2 图片懒加载:IntersectionObserver
    • 八、JavaScript 性能优化知识图谱
    • 九、互动交流

前言

在前端开发中,“快” 是用户体验的核心 —— 页面加载慢会导致用户流失,交互卡顿会降低使用意愿。本文基于实战视角,拆解 JavaScript 性能优化的核心技术,配套 可运行代码示例、高频踩坑点 及 工具使用指南,帮你系统性解决性能瓶颈。

一、优化原则与核心指标:找准优化方向

性能优化不是 “盲目调优”,需先明确 核心指标 和 优化目标,避免 “为了优化而优化”。

1.1 关键性能指标(Web Vitals)

Web Vitals 是 Google 定义的核心用户体验指标,直接反映用户感知到的性能:

指标名称全称含义优化目标查看方式
LCPLargest Contentful Paint最大内容绘制时间(首屏核心内容加载完成时间)≤2.5s(良好)Chrome DevTools > Lighthouse > 性能报告
FIDFirst Input Delay首次输入延迟(用户首次交互到浏览器响应的时间)≤100ms(良好)Chrome DevTools > Performance 面板
CLSCumulative Layout Shift累积布局偏移(页面元素意外跳动的程度)≤0.1(良好)Chrome DevTools > More Tools > Web Vitals

代码示例:监听 LCP 指标
通过 PerformanceObserver 实时监测 LCP,便于线上排查问题:

// 监听 LCP 指标
new PerformanceObserver((entryList) => {const lcpEntry = entryList.getEntries()[0];const lcpTime = lcpEntry.startTime; // LCP 时间(ms)console.log(`LCP 时间:${lcpTime}ms`);// 上报到监控平台(如埋点系统)// reportToMonitor('LCP', lcpTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

常踩坑点:

  • 误将 “首屏加载完成” 等同于 LCP:LCP 关注的是 “最大内容”(如首屏图片、标题),而非整个首屏,需避免用 “首屏时间” 替代 LCP 评估。
  • 忽略 CLS 优化:动态插入内容(如广告、弹窗)时未预留空间,导致页面跳动,CLS 超标。

1.2 优化目标:三大核心维度

加载速度: 减少资源加载时间(脚本、样式、图片等),让页面快速呈现。
执行效率: 降低 JavaScript 执行耗时,避免主线程阻塞(如密集计算、频繁 DOM 操作)。
内存管理: 避免内存泄漏,防止页面长时间运行后卡顿或崩溃。

二、代码层面优化:从 “写好” 到 “写快”

代码是性能的基石,不合理的代码逻辑会直接导致执行效率低下。以下是高频优化场景:

2.1 减少全局变量,避免内存泄漏

问题本质:
全局变量挂载在 window 上,生命周期与页面一致,若未及时清理,会持续占用内存,甚至导致内存泄漏。
代码示例:优化前 vs 优化后
优化前(风险) 优化后(安全)

// 全局变量,无法回收	
let userData = {};	
function fetchUser() {	
userData = await api.getUser (); // 用完后未清理	
}	
```	```javascript
// 1. 模块封装(ES Module)	
export async function fetchUser() {	
const userData = await api.getUser (); // 局部变量,函数执行后自动回收	
return userData;	
}	
// 2. 立即执行函数(IIFE,兼容非模块环境)(function () {let userData = {}; // 局部作用域async function fetchUser () {userData = await api.getUser ();}})();
|#### 常踩坑点:
- 忘记声明变量:`userData = {}` 未加 `var/let/const`,自动变为全局变量(隐式全局)。
- 闭包滥用:全局函数引用局部变量,导致局部变量无法回收(如 `window.onclick = () => { const data = {}; }`)。### 2.2 事件委托:减少事件监听器数量
#### 原理:
利用事件冒泡,将多个子元素的事件监听统一挂载到父元素上,减少内存占用和初始化时间。#### 代码示例:列表点击事件优化
```html
<!-- 场景:100 个列表项,点击获取 ID -->
<ul id="list"><li data-id="1">item 1</li><li data-id="2">item 2</li><!-- ... 98 个项 ... -->
</ul>
// 优化前:每个 li 绑定事件(100 个监听器)
const lis = document.querySelectorAll('#list li');
lis.forEach(li => {li.addEventListener('click', () => {console.log(li.dataset.id);});
});// 优化后:父元素委托(1 个监听器)
const list = document.getElementById('list');
list.addEventListener('click', (e) => {// 关键:判断事件目标是否为 li(避免父元素自身触发)if (e.target.tagName === 'LI') {console.log(e.target.dataset.id);}
});

常踩坑点:

  • 事件目标判断错误:若 li 内有嵌套元素(如
  • item 1
  • ),e.target 会变成 span,需用 e.currentTarget.querySelector(‘li’) 或 closest(‘li’) 修正。
  • 委托到过高层级:如委托到 document 而非父元素 ul,会增加事件冒泡路径,影响性能。

2.3 避免频繁 DOM 操作:用 DocumentFragment

问题本质:
每次 DOM 操作(如 appendChild)都会触发 回流 / 重绘,频繁操作会导致主线程阻塞。DocumentFragment 是 “虚拟 DOM 容器”,可先在内存中组装 DOM,再一次性插入页面,减少回流次数。
代码示例:批量插入列表项


// 优化前:循环插入 1000 个 li(触发 1000 次回流)
const list = document.getElementById('list');
for (let i = 0; i < 1000; i++) {const li = document.createElement('li');li.textContent = `item ${i}`;list.appendChild(li); // 每次循环都操作 DOM
}// 优化后:用 DocumentFragment 批量插入(仅 1 次回流)
const list = document.getElementById('list');
const fragment = document.createDocumentFragment(); // 内存中的容器for (let i = 0; i < 1000; i++) {const li = document.createElement('li');li.textContent = `item ${i}`;fragment.appendChild(li); // 操作内存,不触发回流
}list.appendChild(fragment); // 一次性插入页面,触发 1 次回流

常踩坑点:

  • 误用 innerHTML 替代:list.innerHTML +=
  • item ${i}
  • 虽比循环 append 快,但会覆盖原有事件监听器,且存在 XSS 风险(若内容含用户输入)。

2.4 节流(throttle)与防抖(debounce):控制高频函数

原理对比:

  • 防抖(debounce):高频触发后,等待指定时间再执行,若期间再次触发则重置时间(如搜索框输入联想)。
  • 节流(throttle):高频触发时,每隔指定时间只执行一次(如滚动加载、窗口 resize)。
    代码示例:完整实现
// 1. 防抖函数
function debounce(fn, delay = 300) {let timer = null;return (...args) => {clearTimeout(timer); // 重置定时器timer = setTimeout(() => {fn.apply(this, args); // 执行目标函数}, delay);};
}// 用法:搜索框输入联想
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {console.log('搜索关键词:', e.target.value);// fetch(`/api/search?keyword=${e.target.value}`);
}, 500));// 2. 节流函数(时间戳版)
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(() => {const scrollTop = document.documentElement.scrollTop;const scrollHeight = document.documentElement.scrollHeight;if (scrollTop + window.innerHeight >= scrollHeight - 100) {console.log('触发滚动加载');// loadMoreData();}
}, 500));

常踩坑点:
1.防抖 / 节流混用:如滚动加载用了防抖,会导致用户滚动到底部后需等待 delay 才加载,体验差;搜索输入用了节流,会导致输入过程中频繁触发请求,浪费资源。
2.忘记绑定 this:若目标函数依赖 this(如类方法),未用 apply/call 绑定会导致 this 指向错误。

三、加载与执行优化:让资源 “快” 起来

加载阶段是性能瓶颈的重灾区,合理控制脚本加载顺序和时机,可显著提升首屏速度。

3.1 异步加载脚本:async 与 defer

区别对比:

属性加载时机执行时机执行顺序适用场景
无属性阻塞 HTML 解析加载完成后立即执行按脚本顺序核心脚本(如初始化框架)
async不阻塞 HTML 解析加载完成后立即执行不保证顺序独立脚本(如统计脚本、广告)
defer不阻塞 HTML 解析HTML 解析完成后执行按脚本顺序依赖顺序的脚本(如 jQuery 后加载插件)

代码示例:脚本加载方式


<!-- 1. 同步加载(阻塞 HTML 解析,不推荐) -->
<script src="core.js"></script><!-- 2. async 加载(独立脚本,执行顺序不确定) -->
<script src="analytics.js" async></script><!-- 3. defer 加载(按顺序执行,HTML 解析后执行) -->
<script src="jquery.js" defer></script>
<script src="jquery-plugin.js" defer></script> <!-- 依赖 jquery,顺序正确 -->

常踩坑点:

  • async 脚本依赖顺序:若 script1.js 依赖 script2.js,但两者都加了 async,可能出现 script1 先执行导致报错。
  • defer 脚本放头部:虽然 defer 不阻塞解析,但脚本仍会在头部加载,若脚本体积过大,会占用首屏加载带宽,建议放 底部或用动态加载。

3.2 按需加载模块:动态 import ()

原理:
通过 import() 语法(ES 模块特性),在需要时才加载模块,减少首屏资源体积(如点击按钮后加载弹窗组件)。
代码示例:点击加载弹窗模块

// 弹窗模块:modal.js
export function openModal(content) {const modal = document.createElement('div');modal.className = 'modal';modal.innerHTML = `<div class="modal-content">${content}</div>`;document.body.appendChild(modal);
}// 主脚本:按需加载
const openModalBtn = document.getElementById('openModal');
openModalBtn.addEventListener('click', async () => {// 点击时才加载 modal 模块(返回 Promise)const { openModal } = await import('./modal.js');openModal('这是按需加载的弹窗');
});

常踩坑点:
未处理加载失败:import() 可能因网络错误失败,需加 try/catch 捕获:

try {const { openModal } = await import('./modal.js');
} catch (err) {console.error('模块加载失败:', err);alert('加载出错,请重试');
}

3.3 代码拆分:Webpack/Vite 配置

原理:
通过构建工具(Webpack/Vite)将代码拆分为多个 “chunk”,首屏只加载核心 chunk,其他 chunk 按需加载(如路由拆分、第三方库拆分)。
代码示例 1:Webpack 拆分第三方库(splitChunks)

// webpack.config.js
module.exports = {optimization: {splitChunks: {chunks: 'all', // 拆分所有类型的 chunk(同步/异步)cacheGroups: {// 拆分第三方库(如 react、lodash)到 vendor.chunk.jsvendor: {test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 中的文件name: 'vendor', // chunk 名称priority: 10, // 优先级(高于默认配置)reuseExistingChunk: true, // 复用已存在的 chunk},},},},
};

代码示例 2:Vite 路由拆分(Vue 项目)
Vite 自带代码拆分能力,无需额外配置,只需用动态 import 定义路由:


// router/index.js(Vue Router)
import { createRouter, createWebHistory } from 'vue-router';const routes = [{path: '/',name: 'Home',component: () => import('../views/Home.vue'), // 按需加载 Home 组件},{path: '/about',name: 'About',component: () => import('../views/About.vue'), // 按需加载 About 组件},
];const router = createRouter({history: createWebHistory(),routes,
});export default router;

常踩坑点:

  • 拆分过细:若将代码拆分为过多小 chunk,会导致 HTTP 请求数量增加(虽有 HTTP/2 多路复用,但仍有开销),建议合理控制 chunk 大小(如 Webpack 配置 minSize: 30000,仅拆分大于 30KB 的 chunk)。

四、数据与算法优化:提升执行效率

JavaScript 执行效率的核心是 “数据处理速度”,合理选择数据结构和算法,可降低时间复杂度。

4.1 选择合适的数据结构:Map 替代对象

对比:Map vs 普通对象

特性普通对象Map
键类型仅字符串 /.Symbol任意类型(数字、对象、函数等)
查找速度O (1)(但键名需哈希)O (1)(性能更稳定,尤其是大数据量)
遍历效率需遍历 Object.keys()原生支持 forEach/for…of,效率更高
代码示例:大数据量键值对存储
// 场景:存储 10 万条用户数据(键为用户 ID,值为用户信息)
const userId = 123456;
const userData = { name: '张三', age: 25 };// 优化前:普通对象
const userMap = {};
// 插入 10 万条数据
for (let i = 0; i < 100000; i++) {userMap[`user_${i}`] = { id: i, name: `用户${i}` };
}
// 查找(性能一般)
console.log(userMap[`user_${userId}`]);// 优化后:Map
const userMap = new Map();
// 插入 10 万条数据
for (let i = 0; i < 100000; i++) {userMap.set(`user_${i}`, { id: i, name: `用户${i}` });
}
// 查找(性能更优)
console.log(userMap.get(`user_${userId}`));

常踩坑点:
Map 键的引用类型问题:若用对象作为 Map 键,需注意引用地址是否一致,例如:

const map = new Map();
const key1 = { id: 1 };
const key2 = { id: 1 };
map.set(key1, 'value');
console.log(map.get(key2)); // undefined(key1 和 key2 是不同对象)

4.2 优化循环逻辑:减少嵌套与冗余

核心原则:

  • 减少循环嵌套(嵌套层级越多,时间复杂度越高,如 O (n²) → O (n))。
  • 提前终止循环(用 break/return 避免不必要的迭代)。
  • 避免循环内执行耗时操作(如 DOM 操作、复杂计算)。
    代码示例:循环优化对比
// 场景:从用户列表中找到年龄 > 18 的第一个用户
const users = [{ id: 1, age: 16 },{ id: 2, age: 20 },{ id: 3, age: 22 },// ... 1000 条数据 ...
];// 优化前:嵌套循环 + 无提前终止
let targetUser = null;
for (let i = 0; i < users.length; i++) {const user = users[i];// 冗余:每次循环都判断(可合并到外层条件)if (user) {if (user.age > 18) {targetUser = user;// 忘记 break,继续遍历后续数据}}
}// 优化后:单层循环 + 提前终止
let targetUser = null;
for (let i = 0; i < users.length; i++) {const user = users[i];if (user?.age > 18) { // 可选链简化判断targetUser = user;break; // 找到后立即终止循环,减少迭代次数}
}

常踩坑点:
用 forEach 处理大数据量:forEach 无法提前终止(break 无效),大数据量下建议用 for/for…of 循环。

4.3 Web Workers: offload 密集型计算

原理:
JavaScript 是单线程语言,密集型计算(如大数据排序、复杂数学运算)会阻塞主线程,导致页面卡顿。Web Workers 可开启 “子线程” 处理计算,不影响主线程交互。
代码示例:主线程 + Worker 通信

1.Worker 脚本(compute.worker.js):处理密集计算

// 监听主线程消息
self.addEventListener('message', (e) => {const { data } = e; // 接收主线程传递的参数(如待排序数组)// 密集计算:排序 100 万条随机数const sortedData = data.sort((a, b) => a - b);// 向主线程发送结果self.postMessage(sortedData);// 关闭 Worker(避免内存泄漏)self.close();
});
主线程脚本:发起计算并接收结果
javascript
运行
// 检查浏览器是否支持 Web Workers
if (window.Worker) {// 创建 Worker 实例const computeWorker = new Worker('./compute.worker.js');// 生成 100 万条随机数(密集计算的数据源)const randomData = Array.from({ length: 1000000 }, () => Math.random() * 10000);// 向 Worker 发送数据computeWorker.postMessage(randomData);// 接收 Worker 处理结果computeWorker.addEventListener('message', (e) => {const sortedData = e.data;console.log('排序完成,前 10 条数据:', sortedData.slice(0, 10));});// 处理 Worker 错误computeWorker.addEventListener('error', (err) => {console.error(`Worker 错误:${err.message}(行号:${err.lineno}`);});
} else {alert('您的浏览器不支持 Web Workers,无法进行密集计算');
}

常踩坑点:

  • Worker 操作 DOM:Worker 线程没有 DOM 权限,若在 Worker 中写 document.getElementById() 会报错。
  • 忘记关闭 Worker:若频繁创建 Worker 且不关闭,会导致线程堆积,占用内存,需在计算完成后调用 self.close()(Worker 内)或 computeWorker.terminate()(主线程)。

五、浏览器渲染优化:避免 “卡顿” 与 “跳动”

浏览器渲染流程(解析 HTML → 生成 DOM 树 → 生成 CSSOM 树 → 布局 → 绘制 → 合成)中,布局(回流) 和 绘制(重绘) 是性能瓶颈,需尽量减少。

5.1 减少重绘与回流:用 transform 替代 top/left

关键概念:

  • 回流(重排):DOM 元素位置、尺寸变化导致浏览器重新计算布局,开销大。
  • 重绘:DOM 元素样式变化(如颜色、背景)但不影响布局,开销较小。
  • 代码示例:动画优化(transform vs top/left)
<!-- 场景:实现元素从左到右的动画 -->
<div id="box" style="width: 100px; height: 100px; background: red; position: absolute;"></div>
javascript
运行
// 优化前:用 top/left 动画(触发频繁回流)
const box = document.getElementById('box');
let left = 0;
setInterval(() => {left += 1;box.style.left = `${left}px`; // 每次修改 left 都会触发回流
}, 16);// 优化后:用 transform 动画(仅触发合成,无回流/重绘)
const box = document.getElementById('box');
let x = 0;
setInterval(() => {x += 1;box.style.transform = `translateX(${x}px)`; // transform 由 GPU 处理,不触发回流
}, 16);

常踩坑点:
transform 加 position: fixed:若元素同时设置 transform 和 position: fixed,fixed 会失效(变为相对父元素定位),需避免同时使用。

5.2 requestAnimationFrame:优化动画时序

原理:
setInterval/setTimeout 动画可能因主线程阻塞导致 “掉帧”,而 requestAnimationFrame 会与浏览器刷新频率同步(通常 60fps,即每 16.6ms 执行一次),确保动画流畅。
代码示例:用 requestAnimationFrame 实现动画

const box = document.getElementById('box');
let x = 0;function animate() {x += 1;box.style.transform = `translateX(${x}px)`;// 若未达到目标位置,继续请求下一帧if (x < 500) {requestAnimationFrame(animate);}
}// 启动动画
requestAnimationFrame(animate);

常踩坑点:
在 requestAnimationFrame 内执行密集计算:若 animate 函数内有复杂逻辑(如循环处理大数据),会阻塞动画帧,导致卡顿,需将计算移到 Web Workers 中。

5.3 启用 GPU 加速:will-change 属性

原理:
will-change 告诉浏览器 “该元素即将变化”,浏览器会提前分配 GPU 资源,减少变化时的性能开销(如动画、transform 变化)。
代码示例:提前优化动画元素

/* 告诉浏览器:box 元素的 transform 属性即将变化,提前准备 GPU 加速 */
#box {will-change: transform;/* 其他样式 */width: 100px;height: 100px;background: red;
}

常踩坑点:
滥用 will-change:若给大量元素加 will-change: transform,会导致 GPU 资源占用过高,甚至引发卡顿,建议仅给 “即将动画的元素” 添加,且动画结束后移除(如通过 JS 动态添加 / 删除类)。

六、工具与监控:定位性能瓶颈

“优化” 的前提是 “找到瓶颈”,以下工具可帮助你精准定位性能问题。

6.1 Chrome DevTools:前端性能分析利器

核心面板:Performance
用于录制页面加载、交互过程,分析主线程阻塞、回流 / 重绘等问题。
操作步骤:
1.打开 Chrome 开发者工具(F12 或 Ctrl+Shift+I),切换到 Performance 面板。
2.点击左上角的 录制按钮(圆形图标),然后操作页面(如刷新、点击按钮)。
3.操作完成后点击 停止按钮,生成性能报告。

关键分析点:

  • 长任务:主线程中执行时间超过 50ms 的任务,会导致页面卡顿,需拆分(如用 Web Workers、分批处理)。
  • 回流 / 重绘:在 “Main” 线程中查找 “Layout”(回流)和 “Paint”(重绘)任务,频繁出现则需优化 DOM 操作。

6.2 内存泄漏检测:Heap Snapshots

原理:
通过拍摄 “内存快照”,对比不同时间点的内存占用,定位未回收的对象(如未关闭的定时器、未移除的事件监听器)。
操作步骤:
1.打开 Chrome DevTools → 切换到 Memory 面板。
2.选择 Heap snapshot,点击 Take snapshot 拍摄初始快照。
3.操作页面(如反复点击按钮、切换路由),然后拍摄第二次快照。
4.对比两次快照,查看 “Retainers”(引用链),找出未回收的对象。
代码示例:内存泄漏场景(未移除事件监听器)

// 泄漏代码:每次点击按钮都会添加新的事件监听器,且未移除
const btn = document.getElementById('leakBtn');
btn.addEventListener('click', () => {const largeData = new Array(1000000).fill('leak'); // 大对象btn.addEventListener('mouseover', () => {console.log(largeData); // 引用 largeData,导致无法回收});
});

常踩坑点:

  • 忽略闭包泄漏:若事件监听器、定时器引用了外部变量,且未清理,会导致变量无法回收,需在组件卸载 / 页面关闭时移除监听器(removeEventListener)或清除定时器(clearTimeout)。

6.3 持续监控:Performance API

原理:
通过浏览器原生的 Performance API,在代码中埋点,实时记录关键操作的耗时(如函数执行时间、接口请求时间),并上报到监控平台,实现线上性能监控。
代码示例:记录函数执行时间

// 工具函数:记录函数执行时间
function measureTime(fn, fnName) {// 开始计时const startTime = performance.now();// 执行目标函数const result = fn();// 结束计时const endTime = performance.now();// 计算耗时(ms)const duration = endTime - startTime;console.log(`${fnName} 执行耗时:${duration.toFixed(2)}ms`);// 上报到监控平台// reportToMonitor('function_performance', { fnName, duration });return result;
}// 用法:记录数据处理函数的耗时
function processLargeData(data) {return data.filter(item => item.value > 100).map(item => item.id);
}// 生成测试数据
const largeData = Array.from({ length: 100000 }, () => ({id: Math.random(),value: Math.random() * 200,
}));// 测量执行时间
measureTime(() => processLargeData(largeData), 'processLargeData');

七、实战案例:从理论到落地

7.1 列表渲染优化:虚拟滚动

场景:
渲染 10 万条列表数据,若全部渲染会导致 DOM 元素过多,页面卡顿。虚拟滚动仅渲染 “可视区域” 内的元素(如屏幕内显示 20 条,仅渲染 20 条),大幅减少 DOM 数量。
核心原理:
1.计算可视区域高度、每条列表项高度 → 得出可视区域可显示的项数。
2.监听滚动事件,计算滚动偏移量 → 得出当前可视区域的起始索引。
3.仅渲染起始索引到 “起始索引 + 可视项数” 的元素,并通过 transform 定位到滚动位置。
代码示例:简化版虚拟滚动

<!-- 虚拟滚动容器 -->
<div id="virtualList" style="width: 300px; height: 500px; overflow-y: auto; border: 1px solid #ccc;"><!-- 占位容器(用于撑起滚动高度) --><div id="placeholder" style="height: 0;"></div><!-- 实际渲染的列表项容器 --><div id="listContainer" style="position: relative; top: 0;"></div>
</div>
const virtualList = document.getElementById('virtualList');
const placeholder = document.getElementById('placeholder');
const listContainer = document.getElementById('listContainer');// 配置项
const ITEM_HEIGHT = 50; // 每条列表项高度(px)
const TOTAL_COUNT = 100000; // 总数据量
const VISIBLE_COUNT = Math.ceil(virtualList.clientHeight / ITEM_HEIGHT); // 可视区域项数(如 10)
const BUFFER_COUNT = 5; // 缓冲项数(避免滚动时空白)
const RENDER_COUNT = VISIBLE_COUNT + BUFFER_COUNT * 2; // 实际渲染项数(如 20)// 生成模拟数据
const data = Array.from({ length: TOTAL_COUNT }, (_, i) => `列表项 ${i + 1}`);// 初始化:设置占位容器高度(撑起滚动条)
placeholder.style.height = `${TOTAL_COUNT * ITEM_HEIGHT}px`;// 渲染可视区域的列表项
function renderVisibleItems(startIndex) {// 计算结束索引(起始索引 + 实际渲染项数)const endIndex = Math.min(startIndex + RENDER_COUNT, TOTAL_COUNT);// 截取可视区域数据const visibleData = data.slice(startIndex, endIndex);// 清空容器(避免重复渲染)listContainer.innerHTML = '';// 渲染列表项visibleData.forEach((item, index) => {const li = document.createElement('div');li.style.height = `${ITEM_HEIGHT}px`;li.style.lineHeight = `${ITEM_HEIGHT}px`;li.style.borderBottom = '1px solid #eee';li.textContent = item;// 计算当前项的位置(相对于容器)li.style.position = 'absolute';li.style.top = `${index * ITEM_HEIGHT}px`;li.style.width = '100%';listContainer.appendChild(li);});// 定位列表容器(通过 transform 避免回流)listContainer.style.transform = `translateY(${startIndex * ITEM_HEIGHT}px)`;
}// 监听滚动事件(节流优化)
virtualList.addEventListener('scroll', throttle(() => {// 计算当前滚动偏移量const scrollTop = virtualList.scrollTop;// 计算起始索引(滚动偏移量 / 项高度,向下取整)const startIndex = Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER_COUNT);// 渲染可视区域项renderVisibleItems(startIndex);
}, 16));// 初始渲染
renderVisibleItems(0);

常踩坑点:

  • 滚动节流间隔过大:若节流间隔超过 16ms(60fps 刷新间隔),会导致滚动时出现空白;建议用 requestAnimationFrame 替代 setTimeout 节流。
  • 项高度不固定:若列表项高度动态变化(如内容换行),需提前计算每条项的实际高度,否则会导致定位错误。

7.2 图片懒加载:IntersectionObserver

场景:
页面中有大量图片,首屏加载时仅加载可视区域的图片,滚动到可视区域再加载其他图片,减少首屏加载时间。
代码示例:基于 IntersectionObserver 实现懒加载

<!-- 图片标签:用 data-src 存储真实图片地址,src 存储占位图 -->
<img class="lazy-img" data-src="image1.jpg" src="placeholder.jpg" alt="图片1" style="width: 100%; height: 300px; object-fit: cover;">
<img class="lazy-img" data-src="image2.jpg" src="placeholder.jpg" alt="图片2" style="width: 100%; height: 300px; object-fit: cover;">
<img class="lazy-img" data-src="image3.jpg" src="placeholder.jpg" alt="图片3" style="width: 100%; height: 300px; object-fit: cover;">
javascript
运行
// 检查浏览器是否支持 IntersectionObserver
if ('IntersectionObserver' in window) {// 创建观察者实例const lazyObserver = new IntersectionObserver((entries, observer) => {entries.forEach(entry => {// 图片进入可视区域if (entry.isIntersecting) {const img = entry.target;// 将 data-src 赋值给 src,加载真实图片img.src = img.dataset.src;// 加载完成后,停止观察该图片(避免重复触发)observer.unobserve(img);}});}, {rootMargin: '100px 0px', // 提前 100px 开始加载(优化体验)threshold: 0.1, // 图片 10% 进入可视区域即触发});// 观察所有懒加载图片const lazyImgs = document.querySelectorAll('.lazy-img');lazyImgs.forEach(img => {lazyObserver.observe(img);});
} else {// 降级处理:不支持 IntersectionObserver 时,直接加载所有图片const lazyImgs = document.querySelectorAll('.lazy-img');lazyImgs.forEach(img => {img.src = img.dataset.src;});
}

常踩坑点:
首屏图片懒加载:若首屏图片也加了懒加载,会导致首屏 LCP 时间变长,需排除首屏图片(如给首屏图片加 class=“no-lazy”)。

八、JavaScript 性能优化知识图谱

JavaScript 性能优化
核心指标
代码优化
加载优化
数据与算法优化
渲染优化
工具与监控
实战案例
LCP:最大内容绘制
FID:首次输入延迟
CLS:累积布局偏移
减少全局变量
事件委托
DocumentFragment 批量DOM
节流与防抖
async/defer 异步脚本
动态 import按需加载
Webpack/Vite 代码拆分
Map 替代对象
循环优化
Web Workers 密集计算
transform 避免回流
requestAnimationFrame 动画
will-change GPU 加速

九、互动交流

性能优化是一个 “持续迭代” 的过程,没有 “一劳永逸” 的方案。欢迎在评论区分享你的实战经验:
1.你在项目中遇到过最棘手的性能问题是什么?如何解决的?
2.对于本文提到的优化技术,你有哪些补充或疑问?
3.你常用的性能监控工具或方案是什么?
期待你的分享,一起打造更 “快” 的前端体验!

http://www.dtcms.com/a/470249.html

相关文章:

  • 网上公司注册申请的流程江西短视频搜索seo推荐
  • 网站建设哪家好知道数字化档案馆及网站的建设
  • 汽车行业密钥灌装解决方案:构建可信的车载安全启动与通信体系
  • Vue2+Django TodoList项目跨域解决方案实战
  • 网页结构解析入门:HTML、CSS、JS 与爬虫的关系
  • Mac查看本机发出请求的IP地址
  • 《基于 YOLOv11 的武器装备视觉检测系统构建与专 利申请指南》
  • 云原生时代:微服务架构与Serverless实践指南
  • 3dgs Scene详解
  • 韩国网站设计风格网页即时聊天
  • 用 Jetpack Compose 实现仿网易云音乐播放页 + 歌词滚动
  • 既然根据时间可推算太阳矢量,为何还需要太阳敏感器?
  • 做娱乐新闻的网站有哪些网站建设教材
  • ORACLE数据库字符集
  • 本机做网站服务上传到凡科手机网站建设开发
  • 谷歌和IBM:量子计算新时代即将到来
  • 做那种事免费网站WordPress网站动漫你在
  • ROS 点云配准与姿态估计
  • 活动预告|海天瑞声与您相约 NCMMSC 2025
  • Java入门级教程22——Socket编程
  • 【Linux系统编程】2. Linux基本指令(上)
  • 网站系统介绍如何设置wordpress的内存
  • 毕业设计做网站做不出网站建设手机端pc端分开
  • Git删除本地与远程tag操作指南
  • 爱网站推广优化wordpress第三方登录教程
  • 23种设计模式——享元模式(Flyweight Pattern)
  • 游戏编程模式-享元模式(Flyweight)
  • 新郑做网站优化桂林网站优化公司
  • B站排名优化:知识、娱乐、生活类内容的差异化实操策略
  • 闵行网站制作设计公司昆明哪些做网站建设的公司