☰
前端动画性能优化实战:用 Skills 的 optimize-web-animations 完成离屏动画暂停、内存泄漏审计与 Codex Browser 验证
2026/10/9 5:27:54 网站建设 项目流程

【免费下载链接】Skills

Agent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents

项目地址:https://gitcode.com/gh_mirrors/skills48/Skills
点击查看免费下载

本文以 Skills 仓库中的 optimize-web-animations 技能 为核心,系统拆解一套面向 React/Vite/Next 等前端工程的动画性能优化方法论:从"先测量再动手"的核心原则出发,覆盖 Codex Browser 基线画像、IntersectionObserver 离屏暂停、CSSanimation-play-state门控、RAF 循环直控、内存泄漏加固、行为验证与窄提交全流程。读完本文,你将掌握一套可复用的"只播放可见区域动画"(only play in view)优化方案,并能在 Codex 等 AI 编码代理场景下产出带证据链的性能审计报告。

技能定位:它解决哪一类前端性能问题

根据 SKILL.md 的 frontmatter 描述,optimize-web-animations面向如下场景:Profile、audit 与 optimize 前端页面性能,重点覆盖动画工作负载、内存泄漏风险、长会话(long-session)卡顿、CSS 动画、canvas/WebGLrequestAnimationFrame循环、跑马灯(marquee)、骨架屏(skeleton)、GSAP/Three/Matter 动效、定时器、事件监听器与各种 Observer。

在 agents/openai.yaml 中,该技能对外暴露为 "Web Performance Audit",一句话概括为 "Audit motion and leak risks"。典型触发场景包括:

  • 用户要求"让动画更流畅 / 降低 CPU/GPU 占用";
  • 页面离屏动画仍在空转,需要"暂停离屏动画";
  • 页面用久了越来越慢,需要排查内存泄漏;
  • 滚动卡顿(janky scrolling);
  • 在 React/Vite/Next 前端页面上反复执行"仅可见区域播放"优化。

技能元数据还给出了默认提示词模板:"Use $optimize-web-animations to profile a page for offscreen animation work, memory leak risks, and long-session slowdowns.",说明其默认工作对象正是离屏动画负载、内存泄漏与长会话劣化这三类问题。

核心原则:先测量真实页面,再动手编辑

SKILL.md 用一段 "Core Rule" 定下整条工作流的基调:

Measure the real page before editing. The goal is not to remove motion; it is to make offscreen work stop, visible motion resume correctly, and route/unmount cleanup release long-lived resources.

翻译过来是三层意思:

  1. 先测量,后编辑:任何改动之前,必须先拿到真实页面的性能基线;
  2. 目的不是删除动效:优化目标是让离屏工作停止、可见动效正确恢复,而不是为了指标好看把动画全部删掉(这一点在后面的 "Avoid" 清单中被列为第一禁忌);
  3. 清理要彻底:路由切换与组件卸载时的 cleanup 必须释放长生命周期资源(监听器、观察器、RAF、纹理、媒体流等)。

工具选择上,技能明确要求:可用时优先使用 Codex Browser,尤其针对 localhost 页面;除非用户明确要求,否则不使用 Chrome。这是因为后续所有基线采集、离屏计数与泄漏验证都依赖 Codex Browser 提供的浏览器自动化与playwright.evaluate能力。

七步工作流总览

SKILL.md 将完整优化过程组织为 7 个步骤,构成一条"审查 → 测量 → 修补 → 验证 → 检查 → 提交 → 报告"的证据链闭环:

步骤动作关键产出
1审查仓库上下文动画相关代码清单、脏工作区状态
2采集浏览器基线top/mid/footer/mobile 四档画像、离屏动画计数
3修补控制动效的最小归属者is-offscreen类、CSS 暂停规则、RAF 门控、泄漏加固
4验证行为而非仅构建offscreenRunningCount: 0、可见动效恢复
5运行本地检查git diff --check、lint、build
6窄提交隔离索引,只提交本次任务 hunk
7证据化报告采样路由、计数、局限说明

下面逐步骤展开。

第 1 步:审查仓库上下文,先找到"动效的归属者"

优化前必须弄清动画由谁控制、在哪里定义。SKILL.md 给出的审查要点:

  • 读取AGENTS.md或仓库本地指令,遵守项目约定;
  • 尽早运行git status --short,确认工作区是否干净——如果已经有一堆未提交改动,就要从第一步开始规划窄范围暂存(narrow staging),避免后续误提交无关内容;
  • 定位页面组件、动画 Hook、CSS@keyframes、requestAnimationFrame、setInterval、setTimeout、canvas/WebGL/物理引擎组件、媒体元素、GSAP timeline/tween,以及已有的可见性工具函数;
  • 审查 effect cleanup 是否覆盖了事件监听器、Observer、RAF 循环、interval、定时器、外部脚本、媒体流、WebGL 纹理/材质/几何体/渲染器,以及"卸载后仍可能完成"的异步工作。

这一步的产出是一张"动效归属地图":哪个 DOM 节点、哪个组件、哪段循环在驱动动画。它直接决定第 3 步"最小归属者修补"的目标选择。

第 2 步:用 Codex Browser 采集真实基线

技能反复强调:CSS 性能剖析无法证明 RAF 循环已经停止,canvas/WebGL 元素必须单独检查。基线采集要求:

  • 打开用户指定的精确路由;
  • 分别剖析页面顶部(top)、页面中部(mid)、页脚/下方内容(footer),以及一个移动端视口(当布局可能不同时)——禁止只测页面顶部就下结论;
  • 按计算样式animationName、animationPlayState与可见性统计 CSS 动画数量,必须包含::before和::after伪元素;
  • 单独检查 canvas/WebGL 元素;
  • 记录哪些动画名称在离屏运行,以及负责这些动画的 DOM 归属者;
  • 若是内存/泄漏排查任务,还要记录元素/canvas/图片/iframe 计数、可用的 JS heap 指标、10~30 秒后的空闲采样,以及一次简短的路由循环采样;
  • 如果 heap API 返回null或 Browser 沙箱阻止 monkey-patch,必须如实说明,转而依赖稳定的可观测计数 + 源码审计;
  • 压力测试保持有界(bounded):Browser 标签页崩溃可视为过载证据,但未经最小化测试复现,不得过度归因。

附着到 Codex Browser

references/browser-profiling.md 提供了可直接复用的代码。第一步是通过node_repl附着到 Codex Browser,并尽可能认领用户当前打开的 localhost 标签页:

var browserClientModule = await import('<path-to-browser-plugin>/scripts/browser-client.mjs'); await browserClientModule.setupBrowserRuntime({ globals: globalThis }); globalThis.browser = await agent.browsers.get('iab'); nodeRepl.write(await browser.documentation());

如果上一次剖析尝试把某个标签页弄崩了,不要对崩溃的data:页面做 evaluate——先列出标签页,按 id 认领一个健康的 localhost 标签页,或新开一个干净标签页。

CSS 动画剖析器(profileAnimationPage)

该函数在页面内完成动画枚举与离屏判定:用window.getComputedStyle读取每个元素的animationDuration、animationName、animationPlayState,用getBoundingClientRect判定可见性,元素与伪元素(::before/::after)分别扫描,并限制 DOM 扫描上界为 10000 个元素以保证有界。关键实现如下:

var profileAnimationPage = async (tab, label) => tab.playwright.evaluate((profileLabel) => { const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; const isInView = (rect) => rect.bottom > 0 && rect.top < viewportHeight && rect.right > 0 && rect.left < viewportWidth; const durationPartActive = (part) => { const text = String(part || "").trim(); if (!text || text === "0s" || text === "0ms") return false; const numeric = Number(text.replace("ms", "").replace("s", "")); return Number.isFinite(numeric) && numeric > 0; }; const hasActiveDuration = (value) => String(value || "").split(",").some(durationPartActive); const records = []; const elements = Array.from(document.querySelectorAll("body *")).slice(0, 10000); const pushRecords = (element, pseudo = null) => { const styles = window.getComputedStyle(element, pseudo); if (!styles || !hasActiveDuration(styles.animationDuration)) return; const names = String(styles.animationName || "").split(",").map((name) => name.trim()).filter(Boolean); const states = String(styles.animationPlayState || "").split(",").map((state) => state.trim()); names.forEach((name, index) => { if (!name || name === "none") return; const rect = element.getBoundingClientRect(); records.push({ name, state: states[index] || states[0] || "running", pseudo: pseudo || "element", tag: element.tagName.toLowerCase(), classes: String(element.className || "").slice(0, 180), text: String(element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80), top: Math.round(rect.top), bottom: Math.round(rect.bottom), visible: isInView(rect), }); }); }; elements.forEach((element) => { pushRecords(element, null); pushRecords(element, "::before"); pushRecords(element, "::after"); }); const running = records.filter((record) => record.state !== "paused"); const offscreenRunning = running.filter((record) => !record.visible); const countByName = (items) => items.reduce((acc, item) => { acc[item.name] = (acc[item.name] || 0) + 1; return acc; }, {}); const canvases = Array.from(document.querySelectorAll("canvas")).map((canvas) => { const rect = canvas.getBoundingClientRect(); return { classes: String(canvas.className || "").slice(0, 120), active: canvas.dataset.animationActive || "", width: canvas.width, height: canvas.height, top: Math.round(rect.top), bottom: Math.round(rect.bottom), visible: isInView(rect), }; }); return { label: profileLabel, url: location.href, scrollY: Math.round(window.scrollY || 0), viewport: { width: viewportWidth, height: viewportHeight }, runningAnimationCount: running.length, offscreenRunningCount: offscreenRunning.length, runningByName: countByName(running), offscreenByName: countByName(offscreenRunning), offscreenRunning: offscreenRunning.slice(0, 30), canvases, }; }, label, { timeoutMs: 15000 });

注意两个细节:animation-duration为0s/0ms的动画被过滤(视为无实际动画负载);返回结构中的offscreenRunningCount是贯穿全程的核心指标,第 4 步的通过标准就是它归零。

推荐的测量流程(四档采样)

references/browser-profiling.md给出一次标准测量流程:依次滚动到顶部、42% 高度、页脚附近并各留 500ms 稳定期,再切到 390×844 移动端视口采样:

await tab.playwright.evaluate(() => window.scrollTo(0, 0), undefined, { timeoutMs: 5000 }); await tab.playwright.waitForTimeout(500); var topProfile = await profileAnimationPage(tab, "top"); await tab.playwright.evaluate(() => window.scrollTo(0, Math.floor(document.documentElement.scrollHeight * 0.42)), undefined, { timeoutMs: 5000 }); await tab.playwright.waitForTimeout(500); var midProfile = await profileAnimationPage(tab, "mid"); await tab.playwright.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight - window.innerHeight - 12), undefined, { timeoutMs: 5000 }); await tab.playwright.waitForTimeout(500); var footerProfile = await profileAnimationPage(tab, "footer"); var viewport = await browser.capabilities.get("viewport"); await viewport.set({ width: 390, height: 844 }); await tab.playwright.evaluate(() => window.scrollTo(0, 0), undefined, { timeoutMs: 5000 }); await tab.playwright.waitForTimeout(500); var mobileTopProfile = await profileAnimationPage(tab, "mobile-top"); await viewport.reset();

第 3 步:修补"控制动效的最小归属者"

基线上手后,技能要求只修补能控制动效的最小归属者,而不是大范围重写。

优先复用已有的可见性 Hook

如果应用已经有页面级的 reveal/visibility hook,优先复用它,不要另起炉灶;只有不存在时才引入IntersectionObserver,用它在 section 与动画子元素上切换稳定的类名,例如is-offscreen。

用 CSS 规则暂停离屏 CSS 动画

对 CSS 动画,用目标明确的选择器配合animation-play-state: paused暂停:

main > section.is-offscreen .expensive-animation, .expensive-animation.is-offscreen { animation-play-state: paused !important; }

针对不同场景的补充策略:

  • 重复卡片或占位符:观察卡片外壳(card shell)与其动画后代,而不是观察整个 document——避免选择器过宽导致可见内容也被暂停;
  • 跑马灯/ticker:当所在 section 离屏时暂停整条轨道;
  • 骨架屏与伪元素光泽效果:需要时补上::before/::after的暂停选择器,因为animation-play-state并不会自动覆盖伪元素动画。

Canvas/WebGL/物理循环:直接门控 RAF

技能明确指出:CSSanimation-play-state无法暂停 JavaScript 渲染循环,因此对 canvas/WebGL/物理引擎必须直接门控requestAnimationFrame循环:

  • 当 canvas/容器进入视口相交时启动;
  • 离屏时cancelAnimationFrame;
  • 重新进入时恢复;
  • cleanup 时断开 Observer 并取消帧;
  • 可添加非视觉调试标记data-animation-active,便于浏览器侧验证循环是否真的停止。

尊重 prefers-reduced-motion

如果组件已经处理了prefers-reduced-motion,修补时保持尊重;同时避免为滚动/动画状态引入 React 渲染循环(render loop),防止优化动画反而制造新的性能与一致性隐患。

内存泄漏加固清单

技能给出了面向长生命周期的完整加固项(leak hardening):

  • 清除 effect 创建的所有 timeout/interval;
  • 在卸载前与循环重启前取消 RAF;
  • 断开IntersectionObserver、ResizeObserver、MutationObserver与自定义订阅;
  • 以同一 handler 引用移除全局/window/document 监听器(引用不一致会导致移除失效);
  • 释放 Three/WebGL 的纹理、材质、几何体、渲染器,并移除渲染器 DOM 节点;
  • 杀掉针对 DOM 节点与可变对象(如 shader uniforms)的 GSAP tween/timeline;
  • 停止媒体流,暂停被分离的 video/audio 源;
  • 用isDisposed标志守卫异步加载器,若资源在卸载后才 resolve,则直接释放;
  • React cleanup 中,若 lint 警告 ref 可能变化,要在 effect 内部先捕获ref.current再返回 cleanup;
  • 可见性暂停后,对物理/仿真帧差设置上限(frame delta caps),防止积压的延迟帧触发超大更新。

第 4 步:验证行为,而不是只验证构建

技能强调 "Verify behavior, not just builds"——npm run build通过不代表优化生效。验证要求:

  1. 重新加载路由,重跑同一组 top/mid/footer/mobile 画像;
  2. 目标结果:被测页面区段的offscreenRunningCount: 0;
  3. 确认可见动画仍然运行,或滚动进入视口后能正确恢复;
  4. 确认 RAF/canvas 循环在离屏时报告 inactive、在视口内报告 active,或从源码/运行时状态证明已取消;
  5. 泄漏审计要对比路由循环前后与空闲采样前后:多次导航后 DOM/canvas/图片计数应回到同一基线(允许少量正常的异步内容变化);
  6. 执行一次正常页面交互(如搜索、筛选、导航),确保 Observer 不会破坏动态内容;
  7. 检查新开标签页的 console 警告与报错。

第 5 步:运行本地检查

使用仓库常规门禁。对 React/Vite 应用通常是:

git diff --check npm run lint npm run build

已知的非致命警告要与失败项分开报告,避免误导。

第 6 步:窄提交

当仓库/用户指令要求提交时,若工作区存在无关脏改动,使用隔离索引(isolated index),只把属于本任务的 hunk 放入临时索引:

rm -f /tmp/<task>-index GIT_INDEX_FILE=/tmp/<task>-index git read-tree HEAD # Apply only the intended hunks to the temporary index. GIT_INDEX_FILE=/tmp/<task>-index git diff --cached --check GIT_INDEX_FILE=/tmp/<task>-index git commit -m "Pause offscreen <page> animations" git restore --staged <files> 2>/dev/null || true

硬性约束:永远不要从脏工作区大范围暂存文件,除非每个 hunk 都属于当前任务。这与第 1 步的 narrow staging 首尾呼应,保证优化提交干净、可回溯、易审查。

第 7 步:证据化报告

报告的写法直接决定审计的可信度:

  • 以发现开场:什么仍在运行、什么看起来有泄漏风险、什么无法测量;
  • 把源码审计风险与实时 Browser 测量结果分开呈现;
  • 包含精确的采样路由、离屏动画计数、DOM/canvas 计数稳定性、路由循环结果与本地检查结果;
  • 如实说明局限:尤其是 heap 计数器不可用、或 Browser 插桩被沙箱阻止的情况。

推荐的修复模式(Good Fix Patterns)

SKILL.md 总结了 9 条被验证有效的修复模式:

  • section 级is-offscreen+ 元素级is-offscreen双保险:长 section 中折叠线以下(below-the-fold)的子动画仍可能运行,需要两级类名;
  • 每个路由维护共享的可见性选择器常量,例如COURSES_PAGE_ANIMATION_VISIBILITY_SELECTOR;
  • IntersectionObserver阈值约0.01用于动画门控(极小阈值即可,不必等元素完全进入);
  • WebGL/canvas 效果使用直接 RAF 循环控制,CSSanimation-play-state管不住 JS 渲染循环;
  • 物理循环恢复前加帧差上限(frame delta caps);
  • React ref 若被 GSAP/WebGL 效果使用,cleanup 时捕获节点引用(captured cleanup nodes);
  • 图像/视频/纹理/数据加载器加isDisposed守卫;
  • 短时空闲探针与路由循环探针,用于捕获不断累积的 DOM 节点、canvas、iframe 或未释放的媒体。

应当避免的误区(Avoid)

技能明确列出以下反模式,优化实践中必须规避:

  • 删除全部动画只为让画像通过——违背核心原则;
  • 因祖先选择器过宽而暂停了可见的 hero 动效;
  • 假设animation-play-state能覆盖伪元素或 JavaScript RAF 循环;
  • 长页面只信页面顶部的一次测量;
  • 把 heap 计数器不可用当作"没有内存泄漏"的证据;
  • 在 Browser 里跑无界压力循环——应使用有界循环,崩溃照实记录但不夸大因果;
  • 仅凭截图作为性能证明;
  • 让无关的本地 hunk 搭便车混入提交。

深入:内存与泄漏审计采样器

针对"页面越用越慢"类问题,references/browser-profiling.md提供了专门的采样器。注意:Browser heap API 可能不可用,被沙箱阻止时如实返回null,不得谎报。

单点性能采样(samplePerformancePage)

var samplePerformancePage = async (tab, label, scrollY = 0, settleMs = 1600) => { await tab.playwright.evaluate((nextY) => window.scrollTo(0, nextY), scrollY, { timeoutMs: 5000 }); await tab.playwright.waitForTimeout(settleMs); return tab.playwright.evaluate((sampleLabel) => { const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0; const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; const isVisible = (element) => { if (!element || typeof element.getBoundingClientRect !== "function") return true; const rect = element.getBoundingClientRect(); return rect.bottom >= 0 && rect.right >= 0 && rect.top <= viewportHeight && rect.left <= viewportWidth; }; const animations = (document.getAnimations ? document.getAnimations() : []).map((animation) => { const target = animation.effect && animation.effect.target ? animation.effect.target : null; const rect = target && target.getBoundingClientRect ? target.getBoundingClientRect() : null; return { name: animation.animationName || "", playState: animation.playState, visible: target ? isVisible(target) : true, offscreenClass: Boolean(target && target.classList && target.classList.contains("is-offscreen")), tag: target && target.tagName ? target.tagName.toLowerCase() : "", classes: target && typeof target.className === "string" ? target.className.slice(0, 160) : "", top: rect ? Math.round(rect.top) : null, bottom: rect ? Math.round(rect.bottom) : null, }; }); const running = animations.filter((animation) => animation.playState === "running"); const offscreenRunning = running.filter((animation) => !animation.visible || animation.offscreenClass); const canvases = Array.from(document.querySelectorAll("canvas")).map((canvas) => { const rect = canvas.getBoundingClientRect(); return { classes: String(canvas.className || "").slice(0, 120), active: canvas.dataset.animationActive || "", width: canvas.width || 0, height: canvas.height || 0, top: Math.round(rect.top), bottom: Math.round(rect.bottom), visible: isVisible(canvas), }; }); const countByName = (items) => items.reduce((acc, item) => { const name = item.name || "(unnamed)"; acc[name] = (acc[name] || 0) + 1; return acc; }, {}); return { label: sampleLabel, url: location.href, title: document.title, scrollY: Math.round(window.scrollY || 0), viewport: { width: viewportWidth, height: viewportHeight }, memory: performance && performance.memory ? { usedJSHeapSize: performance.memory.usedJSHeapSize, totalJSHeapSize: performance.memory.totalJSHeapSize, jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, } : null, elements: document.querySelectorAll("*").length, canvases: canvases.length, visibleCanvases: canvases.filter((canvas) => canvas.visible).length, images: document.images.length, iframes: document.querySelectorAll("iframe").length, animations: animations.length, runningAnimations: running.length, offscreenRunningCount: offscreenRunning.length, offscreenRunningByName: countByName(offscreenRunning), offscreenRunning: offscreenRunning.slice(0, 20), canvasDetails: canvases.slice(0, 20), }; }, label, { timeoutMs: 15000 }); };

该采样器与 CSS 剖析器的差异在于:它基于document.getAnimations()枚举 Web Animations API 级动画,并额外采集performance.memory(可用时)、DOM 元素总数、canvas/images/iframe 计数——这些是泄漏审计的核心可观测指标。

空闲与路由循环检查(auditLongSession)

路由循环保持有界:3 个循环通常足以发现明显的 DOM/canvas 累积,而不会让剖析器自身变成负载:

var auditLongSession = async (tab, homeUrl, alternateUrl) => { const result = { idle: [], routes: [] }; await tab.goto(homeUrl); await tab.playwright.waitForLoadState({ state: "domcontentloaded", timeoutMs: 10000 }).catch(() => {}); result.idle.push(await samplePerformancePage(tab, "idle-start", 0)); await tab.playwright.waitForTimeout(12000); result.idle.push(await samplePerformancePage(tab, "idle-after-12s", 0)); const countPage = async (label) => { await tab.playwright.waitForTimeout(1400); return tab.playwright.evaluate((sampleLabel) => ({ label: sampleLabel, url: location.href, title: document.title, elements: document.querySelectorAll("*").length, canvases: document.querySelectorAll("canvas").length, images: document.images.length, iframes: document.querySelectorAll("iframe").length, animations: document.getAnimations ? document.getAnimations().length : null, }), label, { timeoutMs: 15000 }); }; for (let index = 1; index <= 3; index += 1) { await tab.goto(alternateUrl); await tab.playwright.waitForLoadState({ state: "domcontentloaded", timeoutMs: 10000 }).catch(() => {}); result.routes.push(await countPage(`alternate-${index}`)); await tab.goto(homeUrl); await tab.playwright.waitForLoadState({ state: "domcontentloaded", timeoutMs: 10000 }).catch(() => {}); result.routes.push(await countPage(`home-${index}`)); } return result; };

结果解读规则

  • 路由循环后 canvas/图片/iframe 计数稳定,是"未滞留可见视觉资源"的有力证据;
  • 元素计数小幅变化可能是异步内容、认证状态或嵌入组件正常收敛所致,要区分对待;单调增长(monotonic growth)才需要深入调查;
  • 若memory为null,如实说明"Browser 运行时未暴露 heap 计数器",不得据此宣称排除了堆泄漏;
  • 若 Browser 沙箱阻止监听器/定时器 monkey-patch,改用源码审计 + 可观测计数取证。

实战示例:demo 中的输入与预期输出

仓库为该技能提供了完整的演示包(见 demo 目录)。demo/input.md 模拟了一份真实的性能问题清单:

  • 三个requestAnimationFrame循环持续运行;
  • 离屏 canvas 仍保持活跃;
  • 每次路由访问都新增 resize 监听器(监听器累积);
  • reduced-motion 用户下跑马灯副本仍在动画。

对应的 demo/expected-output.md 给出了优化验收结果:

  • 动画工作整合进一个调度循环;
  • 离屏时暂停 canvas 与跑马灯;
  • 路由切换时新增监听器清理;
  • 为 reduced-motion 用户禁用连续运动;
  • 验证闲置 CPU 恢复接近基线。

这套输入/输出恰好覆盖了本技能的核心四板斧——RAF 归并、离屏门控、监听器清理、无障碍降级。演示页面 demo/index.html 以"示例输入 / 预期输出"双栏对照形式呈现该工作流,并将 PROMPT.md、input.md、expected-output.md 三份文件串成可复现的演示链路,帮助理解"证据优先的输入输出交接"(evidence-first input and output handoff)到底长什么样。

结语

optimize-web-animations的可贵之处在于它把"性能优化"从经验直觉变成了可验证的工程流程:先有 Codex Browser 的四档基线画像与offscreenRunningCount量化指标,再有最小归属者修补(CSS 暂停规则 + RAF 直控 + 泄漏加固清单),最后用行为验证、本地检查、窄提交与证据化报告收尾。对任何动画密集的营销页、课程页或长列表页面,这套方法都能在"保留动效体验"与"降低 CPU/GPU 与内存负担"之间找到平衡点。若要在自己的 React/Vite/Next 项目中落地,可直接复用 references/browser-profiling.md 中的剖析器与采样器代码,并把is-offscreen+IntersectionObserver模式沉淀为路由级共享常量,即可把"只播放可见区域动画"变成一次性的、可复述的、有证据的优化实践。

【免费下载链接】Skills

Agent skills for designers and builders using Codex, Claude, Cursor, and other AI coding agents

项目地址:https://gitcode.com/gh_mirrors/skills48/Skills
点击查看免费下载
上一篇:Formily 表单受控模式全解:值受控、响应式受控、Schema 受控与动态片段联动的实战指南
下一篇:使用 lark-cli `wiki +space-create` 创建飞书知识空间:命令用法、源码实现与实战解析

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询