JavaScript 性能优化实战:指标、工具、策略与落地清单

1)先定目标:用指标说话,而不是感觉

页面级关键指标(Core Web Vitals)

LCP(最大内容渲染):理想 ≤ 2.5sINP(互动性):理想 ≤ 200msCLS(稳定性):理想 ≤ 0.1
其它实用指标:TTFB、FCP、TTI、TBT、长任务(Long Task > 50ms)占比、内存占用、JS 解析与执行时间、包体体积、请求数量。

原则

设定性能预算(Performance Budget):如“首屏 JS ≤ 170KB gz、图片 ≤ 300KB、请求数 ≤ 30、LCP ≤ 2.5s”。既做 合成监控(Lighthouse/WebPageTest),也做 真实用户监控 RUM(web-vitals + 自建上报),关注 p75/p95。


2)诊断工具与方法

浏览器 Performance 面板:看火焰图,找主线程瓶颈(脚本执行、样式计算、布局、绘制、合成)。Coverage(代码覆盖率):识别未用代码。Lighthouse:一键体检与建议。WebPageTest:细分网络、阻塞、H2/H3、优先级。bundle 分析
webpack-bundle-analyzer
/
rollup-plugin-visualizer
/
source-map-explorer
RUM 上报:用 PerformanceObserver 捕获 Web Vitals 与长任务。


// web-vitals + PerformanceObserver(RUM 示例)
import { onLCP, onINP, onCLS } from 'web-vitals';

function report(metric) {
  navigator.sendBeacon('/rum', JSON.stringify(metric));
}
onLCP(report);
onINP(report);
onCLS(report);

// 监控长任务
new PerformanceObserver((list) => {
  list.getEntries().forEach(e => {
    if (e.duration > 50) report({ name: 'longtask', duration: e.duration, startTime: e.startTime });
  });
}).observe({ entryTypes: ['longtask'] });

3)网络与资源加载:先把“路”修好

3.1 HTTP/H2/H3 与连接优化

启用 HTTP/2 或 HTTP/3,减少队头阻塞。TLS1.3,配置 OCSP Stapling。DNS/TLS 预热:
<link rel="dns-prefetch" href="//cdn.example.com">

<link rel="preconnect" href="https://cdn.example.com" crossorigin>

3.2 资源优先级与资源提示

preload 关键字体/首屏 CSS/首屏 hero 图:


<link rel="preload" as="style" href="/critical.css">
<link rel="preload" as="image" href="/images/hero.avif" imagesrcset="/images/hero.avif 1x, /images/hero@2x.avif 2x">
<link rel="modulepreload" href="/src/entry.js">

fetchpriority(优先级提示):


<img src="/hero.avif" fetchpriority="high" >
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">

prefetch 次级路由/后续资源(低优先级空闲加载)。

3.3 缓存策略

静态资源:
Cache-Control: public, max-age=31536000, immutable
+ 内容哈希。HTML:
no-store
或短缓存 + CDN 边缘渲染(若 SSR)。Service Worker:离线与缓存兜底;谨慎失效策略。

3.4 压缩与图片

Brotli 优先,fallback gzip

图片用 AVIF/WebP,配合
srcset/sizes

loading="lazy"


<img
  src="/img/cover.avif"
  srcset="/img/cover-480.avif 480w, /img/cover-960.avif 960w"
  sizes="(max-width: 600px) 480px, 960px"
  loading="lazy"
  decoding="async"
  >

3.5 CSS/JS 加载策略

Critical CSS 内联(首屏关键 CSS ≤ ~7KB),其余延迟加载。JS:
type="module"
(原生 ESM 支持并默认 defer),非关键脚本
defer
/
async
按路由/组件拆分,动态
import()
,避免首屏加载全站 JS。


4)构建与包体积:让浏览器少干活

4.1 Tree-shaking 与 sideEffects

使用 ESM(import/export),确保库声明 sideEffects 字段,避免错误摇树。避免
require()
、动态
eval
、带副作用的入口。

4.2 依赖与重复

避免整包引入(如
import _ from 'lodash'
),改为
import debounce from 'lodash/debounce'
或用
lodash-es
。移除大体积冗余(如 moment 全量本地化),用 date-fns/dayjs 替代或按需载入 locale。

4.3 Babel/Polyfill 精准投放

设好 browserslist,用
@babel/preset-env
+
useBuiltIns: 'usage'
+
core-js@3

现代浏览器走 现代产物(module),老浏览器走 legacy(nomodule)分流:


<script type="module" src="/bundle.modern.js"></script>
<script nomodule src="/bundle.legacy.js"></script>

4.4 构建器选择与优化

Vite/Esbuild/Rollup/webpack 新版本都更快。开启 minify(terser/esbuild)scope hoistingsplitChunkslong-term caching。使用分析器定位大包与重复依赖。


5)运行时性能:理解引擎,写出“容易被热身”的代码

5.1 V8 等 JIT 优化要点(通用)

对象形状(Hidden Class/Shapes)稳定:同字段顺序构造对象,避免在热路径给对象动态加字段。避免数组稀疏(有“洞”)与混用类型(number/string 混装)。不要用
delete obj.prop
(会降级字典模式),改为
obj.prop = undefined
或重建新对象。避免热路径使用
arguments
,用剩余参数
(...args)
。尽量让函数 单态/少态(monomorphic/polymorphic) 调用,减少 IC 多态退化。


// 坏:动态增加字段 & delete
function makeUser(id) {
  const u = {};
  u.id = id;
  if (Math.random() > 0.5) u.flag = true; // 形状不稳定
  return u;
}

// 好:一次性定义稳定形状
function makeUser(id, flag = false) {
  return { id, flag }; // 字段顺序固定
}

5.2 热路径避免异常机制与装箱

try/catch 放在冷路径;避免频繁装箱/拆箱(如把数字放进
Map<string, any>
乱用)。字符串拼接现代引擎已很快,但在循环里构建大型字符串 prefer array join 或模板累积并 batch

5.3 Map/Set 与数据结构选择

动态键查找用 Map/Set,需要顺序可复现时用 Array + 索引。计算密集型可考虑 TypedArrayWebAssembly(如有瓶颈)。


6)DOM、样式与渲染:避开“布局抖动”,把动画交给合成器

6.1 读写分离,避免 Layout Thrashing


// 坏:交替读写,触发多次回流
box.style.width = '200px';
const h = box.offsetHeight; // 强制同步布局
box.style.height = (h + 10) + 'px';

// 好:先读后写 & 批处理
const h = box.offsetHeight;
requestAnimationFrame(() => {
  box.style.width = '200px';
  box.style.height = (h + 10) + 'px';
});

6.2 动画与合成

使用 transform/opacity 做动画;避免
top/left/width/height
。必要时加
will-change: transform;
(谨慎、短时使用,避免内存膨胀)。大量内容用 虚拟列表(windowing/virtualization)。

6.3 事件与滚动


passive: true
提升滚动性能。节流/防抖高频事件;滚动观测用 IntersectionObserver(做懒加载/曝光统计)。


window.addEventListener('scroll', onScroll, { passive: true });

const onInput = debounce((e) => search(e.target.value), 200);
input.addEventListener('input', onInput);

7)并行与分工:把重活丢给 Worker/Worklet

Web Worker:CPU 密集计算、JSON 大对象解析、图像处理。Comlink 简化 Worker 通信,避免手写
postMessage
OffscreenCanvas 在 Worker 中渲染;Audio/Animation/Paint Worklet 把特定任务交给更合适的线程。requestIdleCallback 在空闲时做非关键任务(如预取、缓存填充)。


// Worker 主线程
import { wrap } from 'comlink';
const worker = new Worker(new URL('./heavy.worker.js', import.meta.url), { type: 'module' });
const api = wrap(worker);
const res = await api.heavyCompute(data); // 像调用本地函数一样

8)内存与 GC:防漏、防抖、可回收

常见泄漏:全局/单例强引用、闭包持有 DOM、未清理的事件监听/定时器、缓存不失效。用 WeakMap/WeakRef/FinalizationRegistry 管理对临时对象的引用。定期用 Heap Snapshot/Allocation Timeline 排查峰值与未释放对象。


// 使用 WeakMap 避免无界缓存
const cache = new WeakMap();
function render(node, data) {
  if (!cache.has(node)) cache.set(node, computeExpensive(data));
  node.textContent = cache.get(node);
}

9)框架实践要点

React

列表加稳定 key;使用
React.memo
/
useMemo
/
useCallback
控制重渲染。避免在 render 中创建新对象/函数(或用 memo 化)。大列表用 react-window / react-virtualized。路由级 代码拆分
React.lazy
+
Suspense
。关注 Server Components / Streaming SSR(降低客户端 JS)。


const List = React.memo(function List({ items }) {
  return items.map(item => <Row key={item.id} item={item} />);
});

Vue

合理使用
computed
(缓存)而非
watch

v-once
渲染静态块。大组件拆分与异步组件;
keep-alive
缓存路由视图。深层响应式对象可用
shallowRef/markRaw
降低代理成本。

Svelte/其它

编译期优化已很强,仍需拆分路由与懒加载、虚拟列表与 Worker。


10)Node.js 侧(若含 SSR/中间层)

保持事件循环轻盈:CPU 密集任务交给
worker_threads
或外部服务。充分利用 流(streaming),避免
readFileSync
大文件。HTTP keep-alive,连接池,Gzip/Brotli。监控与剖析:
clinic.js

—prof

—trace-gc
,定位阻塞。缓存:内存(LRU)、Redis、边缘缓存。对 SSR 采用 分片渲染/Streaming


11)案例:把“卡顿搜索页”救活(从 2.9s LCP → 1.6s,INP p75 450ms → 120ms)

背景

首屏加载 850KB(gz)JS,搜索输入时主线程卡顿;列表 2000 条。

优化步骤

拆包:路由级分包 + 搜索页图表另成异步 chunk;moment→dayjs;去掉未用的组件库图标集。→ JS 降到 320KB。图片与资源优先级:hero 图 AVIF +
fetchpriority=high
+ preload;字体子集化。→ LCP -400ms。输入防抖 + Worker 过滤:输入
debounce 150ms
;数据过滤搬到 Worker。→ INP -250ms。虚拟列表:react-window 渲染 2k→30 可视项。→ TBT 显著下降。读写分离:滚动与高亮样式统一 rAF 批处理。→ 长任务占比下降。

结果

LCP p75:2.9s → 1.6sINP p75:450ms → 120ms包体:850KB → 320KB

核心代码片段:


// 1) 输入防抖
const onInput = debounce((q) => worker.search(q), 150);

// 2) Worker 侧搜索
expose({
  search(query) {
    // 重计算在 Worker,主线程更顺滑
    return fastFuseSearch(data, query);
  }
});

// 3) 虚拟列表(以 react-window 为例)
const Row = ({ index, style }) => <Item style={style} data={items[index]} />;
<List height={600} itemCount={items.length} itemSize={48} width="100%">
  {Row}
</List>

12)常见误区

只看 Lighthouse 分数:请同时做 RUM,关注真实用户 p75/p95。过度预加载:把所有资源都
preload
会挤占关键资源的带宽与优先级。为优化而优化:未测先“抠微优化”往往收益极小;先找瓶颈。无限缓存不失效:immutable 必须配合内容哈希。在热路径频繁创建新函数/对象:造成 GC 压力与 diff 频繁。


13)落地清单

规划

设定性能预算(LCP/INP/CLS、体积、请求数)。 搭建 RUM 上报(web-vitals + 自建端点)。

网络

H2/H3 + TLS1.3;preconnect CDN。 关键资源 preload / modulepreload;合理 prefetch。 静态资源长缓存 + 内容哈希;Brotli。

构建

ESM + Tree-shaking;
sideEffects
正确标注。 按路由/组件分包;动态 import。 精准 polyfill;modern/legacy 分流。 分析器清理大依赖与重复包。

运行时/渲染

对象形状稳定;避免稀疏数组与
delete
。 高频事件节流/防抖;被动监听。 rAF 批处理读写;动画走 transform/opacity。 虚拟列表;长任务迁移至 Worker。 图片 AVIF/WebP +
srcset/sizes
+
loading="lazy"

内存/Node

WeakMap/解除监听/清理定时器。 Node 流式处理与 worker_threads;SSR streaming。


14)可复制代码片段合集

(1)首屏关键 CSS 内联 + 非关键延后


<style>/* critical css */</style>
<link rel="preload" as="style" href="/above-the-fold.css" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/above-the-fold.css"></noscript>

(2)按需加载路由


// React 示例
const ProductPage = React.lazy(() => import('./pages/ProductPage'));
<Route path="/product/:id" element={<Suspense fallback={<Spinner/>}><ProductPage/></Suspense>} />

(3)节流与防抖


export const throttle = (fn, wait = 100) => {
  let last = 0, timer;
  return (...args) => {
    const now = Date.now();
    if (now - last >= wait) {
      last = now; fn(...args);
    } else {
      clearTimeout(timer);
      timer = setTimeout(() => { last = Date.now(); fn(...args); }, wait - (now - last));
    }
  };
};

export const debounce = (fn, wait = 200) => {
  let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
};

(4)Performance 标记


performance.mark('search-start');
// ... 执行搜索
performance.mark('search-end');
performance.measure('search', 'search-start', 'search-end');
console.table(performance.getEntriesByName('search'));

(5)IntersectionObserver 懒加载


const io = new IntersectionObserver((entries, obs) => {
  for (const e of entries) if (e.isIntersecting) {
    const img = e.target; img.src = img.dataset.src; obs.unobserve(img);
  }
}, { rootMargin: '200px' });

document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));

(6)rAF 批处理读写


let pending = false, writes = [], reads = [];
export function scheduleRead(fn){ reads.push(fn); schedule(); }
export function scheduleWrite(fn){ writes.push(fn); schedule(); }
function schedule(){
  if (pending) return; pending = true;
  requestAnimationFrame(() => {
    const r = reads.slice(); reads.length = 0;
    const w = writes.slice(); writes.length = 0;
    for (const fn of r) fn();
    for (const fn of w) fn();
    pending = false;
  });
}

结语

性能优化没有银弹,但有 顺序与方法测量 → 定预算 → 找瓶颈 → 先网络&包体 → 再运行时&渲染 → 并行与内存 → 监控回归。把这套流程嵌进你的交付与 CI(如体积阈值、Lighthouse CI、RUM 警报),性能就会变成“工程能力”,而不是一次性的突击。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
乐高忍者的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容