JavaScript性能优化实战

要说 JavaScript 性能优化这事儿,咱先从一个场景聊起:你打开一个网页,半天白屏;好不容易加载出来了,点个按钮,等两秒才有反应;想往下滑滑看内容,结果页面跟卡住的磁带似的,一顿一顿的 ------ 是不是特崩溃?

其实这些问题,大多都能通过优化 JS 解决。今天咱就掰开揉碎了说,哪些地方能动手,为啥要这么改,再给点能直接抄的代码示例。全是实战经验,看完就能用。

一、先治 "代码跑不动" 的毛病 ------ 别让主线程累垮了

浏览器里有个 "主线程",就像餐厅里的总厨:既要切菜(解析 HTML)、配菜(计算样式),又要炒菜(执行 JS)、摆盘(渲染页面)。要是 JS 代码写得太 "笨",总厨就被死死缠住,其他活儿全耽误,页面自然就卡。

1. 别让代码 "做无用功"------ 重复的事儿咱只干一次

你想啊,要是代码里总重复查同一个元素、算同一个数,就像炒个菜,每次放盐都得重新称一遍,这不纯浪费时间吗?CPU 全耗在这些破事上,用户点个按钮自然反应慢。

**举个例子:循环里的 "坑"**没优化的代码可能这么写:

复制代码
// 每次循环都要查一遍arr的长度,纯瞎折腾
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

优化后,把长度存起来,少算 n 次:

复制代码
// 先把长度存到变量里,循环里直接用
const len = arr.length;
for (let i = 0; i < len; i++) {
  console.log(arr[i]);
}

再比如:反复查 DOM 元素新手常犯的错:

复制代码
// 每次点击都查一次按钮,没必要
document.getElementById('btn').addEventListener('click', () => {
  // 干点啥
});
// 后面又要用这个按钮,又查一次
document.getElementById('btn').style.color = 'red';

优化后,查一次存起来:

复制代码
// 查一次,后面直接用变量
const btn = document.getElementById('btn');
btn.addEventListener('click', () => { /* 干点啥 */ });
btn.style.color = 'red';

还有个小技巧:少用全局变量 全局变量就像把工具藏在仓库最角落,每次拿都得跑老远;局部变量就在手边,伸手就够。能在函数里定义的,就别挂在window上:

复制代码
// 不好的写法:全局变量
window.userName = '张三';
function showName() {
  console.log(window.userName); // 找起来费劲
}

// 好的写法:局部变量
function showName() {
  const userName = '张三'; // 就在函数里,查找快
  console.log(userName);
}
2. 操作页面元素别太 "勤快"------ 别逼浏览器反复 "改画"

浏览器显示页面,跟画家画画一个道理:先定好每个元素的位置大小(这叫 "重排"),再上色(这叫 "重绘")。你要是频繁改元素的样式、位置,浏览器就得反复改布局、反复上色 ------ 就像画家刚画完一块,你立马说 "这里不对重画",换谁都得疯。

**怎么避免?批量操作!**如果要给列表加 10 个新项,别加一个插一个,先在 "草稿纸" 上准备好:

复制代码
// 没优化的写法:加一个插一个,触发10次重排
const list = document.getElementById('list');
for (let i = 0; i < 10; i++) {
  const li = document.createElement('li');
  li.textContent = `第${i}项`;
  list.appendChild(li); // 每次都触发重排
}

// 优化后:先在内存里拼好,最后一次性插入
const list = document.getElementById('list');
const fragment = document.createDocumentFragment(); // 临时"草稿纸"
for (let i = 0; i < 10; i++) {
  const li = document.createElement('li');
  li.textContent = `第${i}项`;
  fragment.appendChild(li); // 先存在草稿纸里,不触发重排
}
list.appendChild(fragment); // 最后插一次,只触发1次重排

**还有个坑:别 "读了又写,写了又读"**浏览器会攒一波样式修改再统一算布局,但你要是先读高度,再改样式,它就会被迫 "立刻算一遍",白忙活:

复制代码
// 坑:读-写-读-写,触发多次布局计算
const box = document.getElementById('box');
box.style.width = '100px'; // 写
const height = box.offsetHeight; // 读(触发强制布局)
box.style.height = height + 'px'; // 写
const width = box.offsetWidth; // 读(又触发一次)

改成 "先读后写",一次搞定:

复制代码
// 先把要读的全读完
const box = document.getElementById('box');
const height = box.offsetHeight;
const width = box.offsetWidth;

// 再集中写样式
box.style.width = '100px';
box.style.height = height + 'px';

二、再解决 "加载太慢" 的问题 ------ 别让用户等得着急

就算代码跑得再快,加载的时候磨磨蹭蹭,用户打开页面半天一片白,体验也白搭。JS 文件太大、加载时机不对,都是常见的坑。

1. 代码拆成 "小包裹"------ 别让用户一上来就扛大包

一个网页可能有首页、详情页、购物车,要是把所有代码打包成一个大文件,用户打开首页时,连详情页、购物车的代码都得一起下载 ------ 这不就跟去超市买瓶水,却要把整个超市搬回家一样吗?纯浪费时间。

怎么拆?用的时候再加载! ES6 的import()能帮你:

复制代码
// 首页只加载首页代码,点"去详情页"再加载详情页代码
document.getElementById('toDetail').addEventListener('click', async () => {
  // 动态加载详情页模块
  const detailModule = await import('./detail.js');
  detailModule.renderDetail(); // 执行详情页逻辑
});

框架里更方便,比如 Vue 的路由懒加载配置路由时,让组件 "按需加载":

复制代码
// 路由配置
const routes = [
  { 
    path: '/', 
    component: () => import('./Home.vue') // 首页代码,打开就加载
  },
  { 
    path: '/detail', 
    component: () => import('./Detail.vue') // 详情页代码,访问时才加载
  }
];
2. 给代码 "瘦个身"------ 去掉没用的 "肥肉"

文件越小,下载越快。开发时很容易写一堆废话,比如调试用的console.log、没用到的函数,这些都得清掉。

工具能帮你干这活 用 Webpack 打包时,开production模式,它会自动压缩代码、删 "死代码":

复制代码
// webpack.config.js 配置
module.exports = {
  mode: 'production', // 生产模式:自动压缩+Tree Shaking
  // 其他配置...
};

比如你引入了 lodash,但只用到debounce,Tree Shaking 会自动把其他没用的代码删掉:

复制代码
// 只引入需要的功能,别引整个库
import { debounce } from 'lodash-es'; // 好:只加载debounce
// import _ from 'lodash'; // 不好:加载整个库,太大
3. 加载时机别搞错 ------ 别让 JS 挡住页面显示

浏览器默认加载 JS 时,会停下来等 JS 下载、执行完,再继续解析 HTML。要是 JS 文件大,页面就一直白屏 ------ 用户还以为网断了。

给 script 加个 "缓行" 标签

  • async:下载时不耽误 HTML 解析,下完就执行(适合独立脚本,比如统计代码):

    复制代码
    <script src="统计脚本.js" async></script>
  • defer:下载时不耽误解析,按顺序执行,等 HTML 全解析完再跑(适合操作 DOM 的脚本):

    复制代码
    <script src="操作DOM的脚本.js" defer></script>

首屏必需的代码,直接嵌在页面里比如首页要显示一个倒计时,代码很短,直接嵌进去,省个请求:

复制代码
<!-- 内联首屏必需的JS,不用单独下载 -->
<script>
  let count = 10;
  const timer = setInterval(() => {
    document.getElementById('countdown').textContent = count--;
    if (count < 0) clearInterval(timer);
  }, 1000);
</script>

三、给主线程 "找个帮手"------ 别让它一个人干重活

JS 是 "单线程" 的,就一个人干所有活。要是遇到处理 10 万条数据这种重活,主线程被缠住,用户点啥都没反应 ------ 页面跟 "冻住" 了一样。

1. 重活交给 "帮手" 干 ------ 用 Web Workers

比如要筛选 10 万条数据,主线程自己干得卡 5 秒,这时候找个 "帮手"(Web Worker),让它去算,主线程继续响应用户操作。

具体咋弄? 先建个 "帮手" 文件(dataWorker.js):

复制代码
// 帮手的工作:接收数据,处理完发回去
self.onmessage = (e) => {
  const bigData = e.data; // 接收主线程发来的大数据
  // 处理数据(比如筛选出价格>100的商品)
  const filteredData = bigData.filter(item => item.price > 100);
  self.postMessage(filteredData); // 把结果发回主线程
};

主线程里调用帮手:

复制代码
// 主线程:创建帮手,发数据,等结果
const worker = new Worker('dataWorker.js'); // 雇个帮手

// 给帮手发数据
worker.postMessage(十万条商品数据);

// 接收帮手返回的结果
worker.onmessage = (e) => {
  console.log('处理好的数据:', e.data); // 拿到结果,更新页面
};

(注意:帮手不能直接操作 DOM,只能干纯计算的活,别让它越界。)

2. 高频操作 "慢一点"------ 别让事件触发太频繁

滚动页面、输入框打字这些事件,一秒能触发几十次。要是每次都执行复杂代码,主线程根本扛不住。

防抖:等用户 "停手" 了再干活比如搜索框,用户打字时别老发请求,等他停 100 毫秒再发:

复制代码
// 防抖函数:n毫秒内没触发,才执行
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer); // 每次触发都重置定时器
    timer = setTimeout(() => {
      fn.apply(this, args); // 延迟后执行
    }, delay);
  };
}

// 用在搜索框上
const searchInput = document.getElementById('search');
searchInput.oninput = debounce((e) => {
  console.log('发请求搜:', e.target.value); // 等用户停100ms再发请求
}, 100);

节流:不管多频繁,按固定节奏干活比如滚动加载更多,不管用户滚多快,每 500 毫秒只加载一次:

复制代码
// 节流函数:每隔n毫秒最多执行一次
function throttle(fn, interval) {
  let lastTime = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastTime >= interval) { // 够时间了才执行
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 用在滚动加载上
window.onscroll = throttle(() => {
  console.log('加载更多数据'); // 每500ms最多执行一次
}, 500);

四、别让内存 "越堆越多"------ 小心内存泄漏

内存泄漏就像家里的垃圾越堆越多,最后没地方放,网页会越来越卡,甚至崩溃。单页应用尤其要注意,用户操作久了,很容易出这问题。

1. 不用的事件监听,赶紧删

比如给window加了滚动监听,页面跳转后这监听还在,就会一直占内存:

复制代码
// 不好的写法:只加监听,不删
window.addEventListener('scroll', handleScroll);

// 好的写法:离开页面时删掉
function setupScroll() {
  window.addEventListener('scroll', handleScroll);
}
function cleanupScroll() {
  window.removeEventListener('scroll', handleScroll); // 删掉监听
}

// 比如在React组件里,用useEffect清理
useEffect(() => {
  window.addEventListener('scroll', handleScroll);
  return () => { // 组件卸载时执行清理
    window.removeEventListener('scroll', handleScroll);
  };
}, []);
2. 定时器不用了,赶紧关

setInterval要是不清理,会一直跑,还可能引用着变量,内存释放不了:

复制代码
// 不好的写法:开了定时器,没关
const timer = setInterval(() => {
  console.log('一直跑...');
}, 1000);

// 好的写法:不用时关掉
const timer = setInterval(() => {
  console.log('跑一会儿...');
}, 1000);

// 比如5秒后关掉
setTimeout(() => {
  clearInterval(timer); // 清理定时器
}, 5000);

最后说句实在话

JS 优化不是炫技,核心就是让用户用着爽:打开快、点着灵、不卡顿。实际开发时,别瞎优化 ------ 先打开 Chrome 的 "性能面板"(F12 -> Performance)录一段操作,看看哪里耗时最长(比如某个函数执行太久、频繁重排、内存一直涨),再针对性改。

毕竟,解决真问题才叫优化,瞎折腾反而浪费时间~ 你平时开发中遇到过哪些性能坑?评论区聊聊?

相关推荐
Devil枫2 小时前
【案例实战】HarmonyOS应用性能优化实战案例
华为·性能优化·harmonyos
一个很帅的帅哥3 小时前
JavaScript事件循环
开发语言·前端·javascript
驰羽3 小时前
[GO]gin框架:ShouldBindJSON与其他常见绑定方法
开发语言·golang·gin
程序员大雄学编程3 小时前
「用Python来学微积分」5. 曲线的极坐标方程
开发语言·python·微积分
云枫晖3 小时前
Webapck系列-初识Webpack
前端·javascript
jiangzhihao05154 小时前
升级到webpack5
前端·javascript·vue.js
哆啦A梦15884 小时前
36 注册
前端·javascript·html
Jose_lz4 小时前
C#开发学习杂笔(更新中)
开发语言·学习·c#
一位代码4 小时前
python | requests爬虫如何正确获取网页编码?
开发语言·爬虫·python