前端性能优化——从分析到落地

0 阅读5分钟

页面卡? 白屏久? 还是滑动掉帧? 性能优化从来不是玄学,先把脉,再对症下药总是对的。

先分析:别在黑暗里改代码

没有数据的优化,多半是感动自己。那怎么分析呢?

  1. Chrome DevTools:Network 看体积与瀑布流,Performance 看主线程 长任务;
  2. Lighthouse:一键给 FCP、LCP、INP 等打分,数据总是最客观的;
  3. 打包分析webpack-bundle-analyzer 等,看谁把 bundle 撑胖了。

本地快速跑一轮 Lighthouse(需已装 Chrome 与 @lhci/cli):

npm i @lhci/cli
npx lighthouse https://你的站点 --only-categories=performance --output html --output-path ./report.html

你会得到report.html(以百度为例,报告没截全):

image.png

核心指标心里要有数:首屏要快(少下载、少阻塞)、交互要顺(少长任务、少布局抖动)。

简单来说,前端性能优化 === 少下载、少解析、少渲染、少阻塞


整体优化思路

从网络 → 渲染 → JS 运行 → 工程化构建,我将在下文逐步展开

一、网络层:疗效最显著

1. 减小资源体积

  • 压缩 JS/CSS/HTML(Terser、Gzip)
  • 图片压缩
  • 去除无用代码:Tree Shaking

2. 减少请求数量

  • 资源合并(如 JS Bundle 合并)
  • 雪碧图(小图标)
  • 图片懒加载(Intersection Observer)
<img src="hero.jpg" alt="首屏" fetchpriority="high" />
<img src="placeholder.png" data-src="below-fold.jpg" alt="非首屏" loading="lazy" />

配合 Intersection Observer: IntersectionObserver 是浏览器提供的 API,用于异步监听元素与视口的交叉状态,性能高效,常用于图片懒加载、无限滚动、曝光统计等场景

const io = new IntersectionObserver((entries) => {
  entries.forEach((e) => {
    if (!e.isIntersecting) return;
    const img = e.target;
    img.src = img.dataset.src;
    io.unobserve(img);
  });
});
document.querySelectorAll('img[data-src]').forEach((img) => io.observe(img));

3. 加强缓存策略

  • 强缓存:Cache-Control: max-age
  • 协商缓存:Last-Modified / ETag
  • 哈希文件名实现增量更新

4. 使用 CDN

  • 静态资源(JS/CSS/ 图片 / 字体)放 CDN
  • 多地域加速,下载距离更短

5. 预连接 / 预加载

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin />

二、渲染层:首屏体验的「面子工程」也是真工程

1. 减少 DOM/CSS 体积

  • 减少 DOM 层级(最好 ≤ 6 层)
  • 减少无用 CSS:清除冗余样式(PurgeCSS)
  • 关键 CSS 内联(首屏直接可用)
<head>
  <style>
    /* 只放首屏必备:布局、字体、主色 */
    body { margin: 0; font-family: system-ui, sans-serif; }
    .hero { min-height: 40vh; }
  </style>
  <link rel="stylesheet" href="/rest.css" />
</head>

2. 避免渲染阻塞

  • CSS 放在
  • JS 放在 body 底部,或使用 async/defer
  • 避免大量同步 JS 阻碍解析

3. 减少重绘重排(Layout/Paint)

  • 避免频繁操作样式
  • 批量更新 DOM
const frag = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `项 ${i}`;
  frag.appendChild(li);
}
listEl.appendChild(frag); // 一次插入,减少多次重排
  • 使用 requestAnimationFrame
requestAnimationFrame(() => {
  box.style.transform = `translateX(${x}px)`; // 优先 transform / opacity,利于合成层
});
  • 动画优先使用 transform/opacity(可直接 GPU 加速)

4. 首屏加速

  • 骨架屏、Skeleton
  • 服务端渲染 SSR / 静态站点 SSG
  • 先渲染核心内容,再加载非首屏资源

三、JS 运行层:主线程是单行道,别堵死

1. 避免主线程阻塞

  • 复杂计算 → Web Worker
// main.js
const w = new Worker('/worker.js');
w.postMessage({ n: 1e8 });
w.onmessage = (e) => console.log('结果', e.data);

// worker.js
self.onmessage = ({ data }) => {
  let s = 0;
  for (let i = 0; i < data.n; i++) s += i % 7;
  self.postMessage(s);
};
  • 避免长任务(≤ 50ms)
// 长任务让出主线程(简单切片)
async function runChunks(items, chunkSize, processItem) {
  for (let i = 0; i < items.length; i += chunkSize) {
    const slice = items.slice(i, i + chunkSize);
    slice.forEach(processItem);
    await new Promise((r) => setTimeout(r, 0)); // 让浏览器喘口气,避免长时间卡死
  }
}
  • 异步化:使用 Promise/async

2. 优化交互逻辑

  • 防抖节流(scroll/resize/input)
// 防抖
function debounce(fn, wait = 300) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn.apply(this, args), wait);
  };
}
input.addEventListener('input', debounce((e) => console.log(e.target.value), 300));
// 节流
function throttle(fn, wait = 200) {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last < wait) return;
    last = now;
    fn.apply(this, args);
  };
}
window.addEventListener('scroll', throttle(() => console.log(window.scrollY), 200));
  • 事件委托减少事件绑定
document.getElementById('list').addEventListener('click', (e) => {
  const btn = e.target.closest('[data-action="del"]');
  if (!btn) return;
  const id = btn.dataset.id;
  console.log('删除', id);
});
  • 虚拟列表(Virtual List)渲染大数据

3. 减少内存泄漏

  • 及时清除事件监听
import { useEffect } from 'react';

// 定时器与监听在卸载时清掉
useEffect(() => {
  const t = setInterval(() => {}, 1000);
  const onVis = () => {};
  document.addEventListener('visibilitychange', onVis);
  return () => {
    clearInterval(t);
    document.removeEventListener('visibilitychange', onVis);
  };
}, []);
  • 避免闭包意外引用
  • 组件卸载清理 state / 定时器

四、工程化:长期收益最高的一刀

1. 代码分割(Code Splitting)

  • 按模块分割,避免超大 bundle(splitChunks)

2. 第三方库优化

  • 按需引入(如 lodash-es)
  • 外部化 CDN 加载大型库(React/Three.js)

3. 构建产物优化

  • 移除 console
  • 压缩混淆(Terser)
  • 模块规范使用 ES Module(支持 Tree Shaking)

4. 分析与监控

  • 使用 Webpack-Bundle-Analyzer / speed-measure-webpack-plugin等先分析
  • Lighthouse 自动化检测
  • 监控 Core Web Vitals(FCP/LCP/INP)
  1. FCP —— First Contentful Paint 首次内容绘制
  • 含义:浏览器第一次渲染出内容(文本、图片、非空白的 canvas 等)的时间点。
  • 代表:页面开始有内容显示,不再是白屏。
  • 越快越好。
  1. LCP —— Largest Contentful Paint 最大内容绘制(最重要的加载指标)
  • 含义:页面最大的那块内容(通常是标题图、首屏标题)渲染完成的时间。
  • 代表:用户感知到页面加载完成的速度。
  • 优秀标准:≤ 2.5s
  • 影响因素:图片大小、服务器响应速度、JS 阻塞
  1. INP —— Interaction to Next Paint 交互到下一次绘制(Google 新核心体验指标)
  • 含义:从用户点击 / 输入 → 到页面真正响应并更新画面的延迟。
  • 代表:页面交互流畅度、是否卡顿
  • 优秀标准:≤ 200ms
  • INP 高 = 点了没反应、页面卡顿、输入延迟

更进一步可参考 # 手把手系列之——Webpack 优化技巧


收尾

整体思路可以从 网络 => 渲染 => JS执行 =>工程化四部分进行,多上手试试,形成自己的优化体系就会轻松很多啦。
切记,先分析再动手,时刻关注数据(即量化指标),拒绝负优化