mcpField
W
web-performance-web-performance

web-performance-web-performance

github.com/agents-inc开发2692 次安装2 天前更新
代码仓库

关于 web-performance-web-performance

面向 Web 开发者的性能优化指南,涵盖 Core Web Vitals、资源预算和运行时模式。强调先测量并定位瓶颈,再选择优化方式。

主要功能

  1. 01Core Web Vitals 指标与改进
  2. 02Bundle 预算及 CI 检查
  3. 03按路由拆分代码
  4. 04长列表虚拟滚动
  5. 05输入防抖与策略性记忆化

适用场景

  1. 01排查页面加载缓慢
  2. 02定位交互卡顿与重复渲染
  3. 03为 Bundle 设置 CI 限额

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-performance-web-performance

安装后在 Claude Code 中输入/web-performance-web-performance确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
code-splitting.md
core.md
image-optimization.md
web-vitals.md

详细介绍

Web 性能模式

快速指南: 三个数字决定页面是否够快:LCP 低于 2.5s、INP 低于 200ms、CLS 低于 0.1。包体积是影响前两项指标的杠杆,因此要为其设定预算——主包压缩后约为 200 KB,并按路由拆分。优化前先测量,优化后也要在生产环境中再次测量,因为实验室得分与真实用户的会话表现并不一致。启用 React Compiler 后,记忆化会自动处理;手写的 useMemo 必须有性能分析结果作为依据。

详细资源:


适用路径

  • 问题出在包体积 — 首次加载缓慢、主代码块过大或依赖过重;请参阅 examples/code-splitting.md。
  • 问题出在运行时 — 列表卡顿、交互缓慢或重复渲染;请参阅 examples/core.md。
  • 尚未进行任何测量 — 先添加监测,因为上述两类问题的解决方案不同,猜测可能会选错;请参阅 examples/web-vitals.md。

<critical_requirements>

优化性能之前

先分析性能,并在做任何更改前指出瓶颈。 每项优化都会牺牲可读性,而对从未缓慢过的代码进行优化毫无收益——性能分析器、包分析或现场测量能将猜测转化为目标。

先写下预算,再开发功能。 在 CI 中设定包体积上限和 Core Web Vitals 目标,意味着今后的每个依赖项都有衡量标准;若事后才添加,这些标准只能说明你已经超标多少。

测量真实会话,而不只是实验室测试。 实验室环境只有一种设备、一种网络和一个冷缓存;现场数据反映的是用户实际使用的设备分布,而两者在最重要的页面上往往最不一致。

只有到达某个路由时才加载该路由的代码。 按路由边界拆分,是大多数应用可采用的最大幅度缩减方式,因为这能避免用户为从未打开过的页面付出加载成本。

</critical_requirements>


自动检测: Core Web Vitals、LCP、INP、CLS、TTFB、包体积、包体积预算、代码拆分、延迟加载、摇树优化、记忆化、React Compiler、虚拟化、虚拟滚动、防抖、节流、性能预算、现场测量、RUM

适用范围:

  • Core Web Vitals:各项指标的测量内容及其影响因素
  • 包体积预算及其在 CI 中的强制执行
  • 代码拆分、动态导入和摇树优化
  • 渲染开销:记忆化、虚拟化、防抖,以及避免占用主线程
  • 图片体积和格式选择

其他内容:

  • 打包工具配置 — 分块策略和分析输出取决于所用的打包工具;本指南用于确定这些数字应设为何值。
  • 服务器响应时间 — TTFB 是这里所有内容的上游指标;缓存、压缩和源站延迟属于服务层。
  • 框架级渲染 — 页面采用服务端渲染、流式渲染还是静态渲染,由框架决定,且各自对 LCP 的影响不同。
  • 数据获取和缓存 — 不必要的重复请求属于数据获取层的问题,不过它会在这里表现为 INP。

## 理念

性能优化通常会以两种方式出错:优化本来就不慢的部分,或者在没有预算的情况下不断添加功能,直到页面处处都变慢。这两种失败的根源都是衡量不当,而非技术问题。

因此,顺序是固定的——先设定预算,再构建,然后衡量,最后优化衡量结果指出的问题。


<decision_framework>

应该把精力花在哪里

Is the problem measured?
├─ NO → Measure it. A profiler for runtime, a bundle report for size,
│        field data for what users actually experience.
└─ YES → Is it load or interaction?
    ├─ Load (LCP, first paint) → How big is the initial download?
    │   ├─ Over budget → Split by route, defer heavy dependencies,
    │   │                 drop or replace the largest one
    │   └─ Within budget → It is the critical path: preload the LCP image,
    │                      remove render-blocking resources, cut TTFB
    └─ Interaction (INP, jank) → What is holding the main thread?
        ├─ A long task → Break it up, or move it to a worker
        ├─ Re-rendering → Profile the tree, then memoise what the profile named
        └─ Too many DOM nodes → Virtualise the list

**是否使用记忆化:**使用 React Compiler 时,答案通常是“编译器已经处理好了”。不使用时,只对频繁重新渲染、属性未变且渲染耗时明显的组件使用记忆化——不要对其他内容使用,因为比较本身也有开销。

**是否使用虚拟化:**超过大约 100 行后,DOM 就会成为开销所在,虚拟化便能带来收益。少于这个数量时,虚拟化弊大于利,而且无论如何都会影响页面内查找和浏览器原生滚动锚定。

</decision_framework>


核心模式

模式 1:包体积预算

预算按产物分别设定,并在 CI 中强制执行,因此,如果某个依赖让一个代码块体积翻倍,拉取请求就会失败,而不是等到上线后才发现。

export const BUNDLE_SIZE_BUDGETS_KB = {
  MAIN_BUNDLE_GZIPPED: 200,
  VENDOR_BUNDLE_GZIPPED: 150,
  ROUTE_BUNDLE_GZIPPED: 100,
  TOTAL_INITIAL_LOAD_GZIPPED: 500,
  CRITICAL_CSS_INLINE: 14, // fits in the first TCP round trip
} as const;

这些数值依据慢速网络下的下载时间制定,而非个人偏好——相关表格和说明见 reference.md,强制执行方法见 examples/code-splitting.md。

模式 2:核心 Web 指标

每项指标的成因各不相同,因此,单一的“让它更快”任务很少能同时改善多项指标。

指标衡量内容常见成因
LCP加载未优化的主视觉图片、阻塞渲染的 CSS 或 JS、较慢的 TTFB
INP交互性主线程上的长任务、JavaScript 过多
CLS视觉稳定性未指定尺寸的图片、后期注入的内容、字体替换

阈值和解决方法见 reference.md;相关模式见 examples/web-vitals.md。

模式 3:路由级代码拆分

每个延迟加载的路由都会成为独立的代码块,在访问该路由时才会获取。

import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./pages/dashboard"));
const Reports = lazy(() => import("./pages/reports"));

// Whatever the router hands you, the lazy component renders under one Suspense boundary
<Suspense fallback={<PageLoader />}>
  {currentPage === "dashboard" ? <Dashboard /> : <Reports />}
</Suspense>;

拆分路由、体积较大的功能模块、对话框和首屏以下的部分。将首屏组件、错误边界和加载状态保留在主包中——延迟加载这些内容会在关键路径上增加一次往返。

完整代码见:examples/code-splitting.md

模式 4:延迟加载较重的依赖

只在某个交互中用到的大型库,应在该交互内部导入,而不是在文件顶部导入。

// "chart-library" stands for whichever heavy dependency the path actually needs
async function renderChart(container: HTMLElement, data: ChartData) {
  const { createChart } = await import("chart-library");
  return createChart(container, data);
}

这样,所有从未触发该路径的用户都无需加载整个库。

模式 5:策略性记忆化

先进行性能分析。使用 React Compiler 时,大部分工作已经由它替你完成。

// Worth it: sorting thousands of rows on every keystroke elsewhere in the tree
const sortedRows = useMemo(
  () => [...rows].sort((a, b) => compareValues(a[sortColumn], b[sortColumn])),
  [rows, sortColumn],
);

// Not worth it: the comparison costs more than the work
const doubled = useMemo(() => value * 2, [value]);

完整代码见:examples/core.md

模式 6:长列表虚拟化

只渲染可见区域,而不是整个集合,这样无论列表增长到多长,DOM 的大小都能保持恒定。

const visible = rows.slice(startIndex, endIndex);

<div style={{ height: rows.length * ROW_HEIGHT_PX }}>
  <div style={{ transform: `translateY(${startIndex * ROW_HEIGHT_PX}px)` }}>
    {visible.map((row) => (
      <Row key={row.id} row={row} />
    ))}
  </div>
</div>;

完整代码见:examples/core.md

模式 7:对输入驱动的工作进行防抖

等用户停止输入后再执行开销较大的响应操作,而不是每次按键都执行。下面的 useDebounced 只用了十几行 useRef 和 setTimeout,完整代码见 examples/core.md。

const debouncedSearch = useDebounced(performSearch, SEARCH_DEBOUNCE_MS);

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
  setQuery(event.target.value);
  debouncedSearch(event.target.value);
};

仅当最终值重要时使用防抖——例如搜索、自动保存和验证。若中间值重要、但不需要频繁处理,则使用节流——例如滚动、调整大小和指针跟踪。

完整代码见:examples/core.md


<red_flags>

风险信号

运行时会出错:

  • 延迟加载的组件上方没有 Suspense 边界——React 会直接抛出错误,而不是等待。
  • 备忘录钩子的依赖数组不稳定——每次渲染都会创建新对象或内联函数,导致备忘录永远无法命中,还要一直付出比较开销。
  • 延迟加载错误边界或加载状态——本应在出错时渲染的内容本身却仍在下载。

出人意料的行为:

  • React.memo 会进行浅比较,因此每次渲染都会创建的新对象属性会让它完全失效。
  • 备忘录机制会带来比较开销并保留引用;对于耗时不到几毫秒的操作,净效果是负面的。
  • 代码分割会增加总体字节数——增加更多代码块和运行时开销——但首次加载仍然更快。
  • 实验室评分和实际数据衡量的是不同人群;实验室审核结果为绿色,而实际场景中的 INP 为红色,这是常见情况,并不矛盾。
  • 虚拟滚动会破坏页面内查找、指向屏幕外行的锚点链接,以及原生滚动恢复。
  • 延迟加载的组件不会出现在服务器渲染的 HTML 中,因此它们到达时,首屏内容会发生位移。
  • 命名空间导入(import _ from "…")和 require() 都会妨碍摇树优化;重新导出整个目录的 barrel 文件也一样。
  • 不注明压缩方式,预算就毫无意义——200 KB 的预算在未压缩、gzip 压缩和 brotli 压缩下是三个不同的限制。
  • AVIF 并非通用格式;如果 <picture> 没有 WebP 或 JPEG 来源,部分用户将无法显示图像。

评论

支持 Markdown