性能优化学习
https://developer.chrome.com/docs/devtools/performance/selector-stats?hlzh-cn在 Chrome Performance 面板里想看Layout重排 / 回流非常直观我给你用最简单的方式讲清楚怎么看、怎么定位。体验https://googlechrome.github.io/devtools-samples/jank/一、先打开 PerformanceF12 →Performance点左上角● 录制刷新页面 / 操作页面点 ■ 停止等待分析结果出来二、怎么识别 Layout主要看这 3 个地方1. 主面板颜色条最直观蓝色条Loading加载、解析 HTML/CSS黄色条ScriptingJS 执行紫色条Rendering→ 这里面就包含Layout绿色条Painting重绘灰色条System / Idle只要看到紫色条特别长、特别多、特别碎就是 Layout 太多、性能差。2. 看 Main 线程任务里的标签点开 Main 线程的火焰图看到Layout字样 → 就是重排看到Forced Synchronous Layout→ 强制同步布局最伤性能看到Recalculate Style→ 重新计算样式一般会跟着 Layout3. 看 Summary 统计底部 Summary 面板Rendering时间占比高 → Layout / 样式计算多三、重点怎么看 “哪个 JS 导致了 Layout”找到紫色的Layout任务点开右边Call Stack调用栈栈顶就是触发 Layout 的 JS 代码位置点文件名直接跳到源码常见触发读写 offsetWidth /clientHeight/getBoundingClientRect频繁改 stylewidth/height/top/left频繁增删 DOM窗口 resize / 滚动四、Forced Synchronous Layout强制同步布局这是性能杀手表现为火焰图里出现密集小紫条任务名标红Forced Synchronous Layout原因JS 一边读布局属性一边改样式浏览器被迫不停重排。五、你关心 ECharts 多图表场景怎么看多 ECharts 刷新时你重点看是不是一堆密集紫色 Layout 条是不是有Forced Synchronous LayoutECharts 容器是否频繁触发尺寸计算resize如果用了transform: translateZ(0)优化后紫色 Layout 条明显变少绿色 Paint 也会减少主要剩下灰色合成Composite六、最简单判断标准紫色条少 → Layout 少 → 流畅紫色条多且碎 → Layout 爆炸 → 卡顿出现红色 Forced Synchronous Layout → 必须优化操作类型触发的完整流程性能开销核心定义示例属性/操作重排 (Reflow / 回流)Layout Paint Composite⚠️ 最高重新计算元素的几何属性位置、大小会触发后续所有渲染阶段影响范围可覆盖整个页面宽高width/height、边距margin/padding、位置top/left、display(none/block)、字体font-size、添加/删除Dom元素改变窗口大小重绘 (Repaint)Paint Composite⚠️ 中等仅重新绘制元素的外观属性不改变几何位置跳过布局计算颜色color/background-color、可见性visibility、透明度opacity, 不提升合成层时、边框样式border-style合成 (Composite)Composite only✅ 最低仅在 GPU 层合并图层不触发布局和绘制是性能最优的操作transform、opacity(提升合成层后)、filter(部分)仅触发合成零开销优先使用通过 GPU 硬件加速仅在合成层操作不触发重排 / 重绘transform平移、缩放、旋转等浏览器默认提升为独立合成层opacity元素被提升为独立合成层后修改仅触发合成部分filter属性如blur需浏览器支持合成层优化will-change提前告知浏览器元素将发生变化主动提升合成层提升合成层用will-change、transform: translateZ(0)主动提升元素为独立图层减少重绘范围transform的优势完全在合成层执行不触发重排 / 重绘是动画性能最优方案合成层由 GPU 管理过多独立图层会增加内存开销需合理控制图层数量合浏览器渲染原理和 ECharts 特性我们可以通过主动提升合成层合理使用 GPU 加速属性大幅降低多图表页面刷新 / 渲染时的重排、重绘开销。一、核心原理回顾ECharts 的canvas渲染本质上是绘制在 DOM 元素上的位图默认情况下这些 canvas 会和页面其他元素共用合成层每次刷新 / 重绘都会触发全页面的 PaintComposite。通过以下方式我们可以让图表容器独立为合成层让后续的动画 / 更新仅触发 GPU 合成不阻塞主线程transform强制提升为独立合成层浏览器默认优化opacity配合合成层修改仅触发 GPU 合成filter部分滤镜如blur可在 GPU 层执行不触发布局重排will-change提前告知浏览器元素将发生变化主动提升合成层二、实战优化方案含代码1. 图表容器强制提升为独立合成层给每个 ECharts 容器添加合成层触发样式让浏览器为其分配独立 GPU 图层避免重绘污染其他元素。/* 方案1使用transform主动提升合成层兼容性最好 */ .echarts-container { /* translateZ(0) 强制开启GPU硬件加速提升为独立合成层 */ transform: translateZ(0); /* 优化渲染性能避免图像抖动 */ backface-visibility: hidden; perspective: 1000; } /* 方案2will-change 提前告知浏览器适合已知会频繁更新的图表 */ .echarts-container { will-change: transform, opacity; }!-- 每个图表容器都应用该样式 -- div classecharts-container idchart1/div div classecharts-container idchart2/div2. 页面刷新 / 初始化时的性能优化1批量初始化图表减少重排次数多图表同时初始化会导致浏览器多次重排通过requestAnimationFrame批量执行初始化合并重排// 错误示范逐个初始化触发多次重排 const chart1 echarts.init(document.getElementById(chart1)); const chart2 echarts.init(document.getElementById(chart2)); // 正确示范批量初始化合并重排 requestAnimationFrame(() { const charts []; // 所有图表DOM节点 const chartContainers document.querySelectorAll(.echarts-container); chartContainers.forEach((dom, index) { const chart echarts.init(dom); // 配置option... charts.push(chart); }); // 保存实例后续更新使用 window.charts charts; });2避免图表容器尺寸重排图表容器尺寸变化会触发重排提前固定容器尺寸或避免动态修改宽高.echarts-container { width: 100%; height: 300px; /* 固定高度避免JS动态修改height */ transform: translateZ(0); }如果需要响应式适配优先使用 CSStransform: scale()缩放容器而非修改width/height.echarts-wrapper { transform: scale(0.8); /* 仅GPU合成不触发布局重排 */ transform-origin: top left; }3. 图表更新 / 动画时仅触发 GPU 合成1数据更新优化避免全量重绘使用 ECharts 的增量更新 API配合合成层让更新仅在 GPU 层完成// 错误示范setOption 全量更新触发重绘重排 chart.setOption(newOption); // 正确示范仅更新变化的数据减少重绘范围 function updateChart(chart, newData) { // 仅更新series数据不修改其他配置 chart.setOption({ series: [{ data: newData, animationDurationUpdate: 0 // 关闭不必要的更新动画减少主线程开销 }] }); }2使用 opacity 实现淡入淡出动画GPU 合成修改合成层上的opacity仅触发 GPU 合成性能远优于修改visibility/display.echarts-container { transform: translateZ(0); transition: opacity 0.3s ease; /* GPU层执行过渡不阻塞主线程 */ }// 显示/隐藏图表仅修改opacity不触发重排/重绘 function toggleChart(chartDom, show) { chartDom.style.opacity show ? 1 : 0; }3filter 属性的 GPU 优化使用部分filter滤镜如blur可在 GPU 层执行适合图表加载时的占位效果.echarts-loading { transform: translateZ(0); filter: blur(2px); /* GPU层执行模糊不触发布局重排 */ opacity: 0.7; }三、进阶优化合成层管理与避坑1. 避免合成层爆炸不要给所有元素都加transform: translateZ(0)过多合成层会增加 GPU 内存开销仅给频繁更新 / 动画的图表容器提升合成层静态图表无需额外处理2. 合成层优化的验证方法使用 Chrome DevTools 查看合成层打开控制台 →More tools→Layers查看每个图表容器是否被标记为独立合成层Composited Layers检查重绘区域使用Rendering面板的Paint flashing确认图表更新时仅自身区域重绘3. 兼容性说明transform: translateZ(0)兼容性所有现代浏览器均支持移动端也兼容will-changeChrome/FF 支持旧版浏览器会忽略不影响基础功能filter部分浏览器对filter的 GPU 加速支持有限优先用于非关键路径四、额外的 ECharts 性能优化配合合成层效果翻倍渲染器选择大数据量场景优先使用renderer: canvas避免 SVG 生成大量 DOM 节点const chart echarts.init(dom, null, { renderer: canvas });数据降采样使用sampling减少渲染数据点降低 canvas 绘制开销option { series: [{ type: line, data: largeData, sampling: lttb // 大数据采样优化保留关键拐点 }] };关闭非必要动画禁用初始渲染和更新动画减少主线程计算option { animation: false, animationDurationUpdate: 0, tooltip: { show: false } // 非必要交互组件可关闭 };五、完整示例多图表页面优化模板style .echarts-container { width: 100%; height: 300px; margin: 10px 0; /* 合成层优化 */ transform: translateZ(0); will-change: opacity; backface-visibility: hidden; } /style div classecharts-container idchart1/div div classecharts-container idchart2/div script // 批量初始化图表合并重排 requestAnimationFrame(() { const chartContainers document.querySelectorAll(.echarts-container); const charts []; chartContainers.forEach((dom, index) { const chart echarts.init(dom, null, { renderer: canvas }); chart.setOption({ xAxis: { type: category, data: [Mon, Tue, Wed] }, yAxis: { type: value }, series: [{ type: line, data: [120, 200, 150], sampling: lttb }], animation: false, animationDurationUpdate: 0 }); charts.push(chart); }); // 后续更新时仅修改数据不触发重排 window.updateCharts (newDataList) { requestAnimationFrame(() { charts.forEach((chart, index) { chart.setOption({ series: [{ data: newDataList[index] }] }); }); }); }; }); /script关键总结合成层优化的核心是让图表容器成为独立 GPU 图层后续的更新 / 动画仅触发合成不阻塞主线程同时配合 ECharts 自身的渲染优化才能实现真正的流畅体验。_01. 代码分离1代码分离默认情况下所有的 JS 代码包括业务代码、三方依赖以及 Webpack 所依赖的模块化代码都会被打包进入一个 bundle 文件中。当访问这个页面时首先会下载这个页面的 HTML然后进行解析当解析到 script 标签时就会下载该标签 src 所引用的资源由于所有的代码都在一个 bundle 文件中所以该文件势必会非常大就会造成白屏时间过长严重影响首页的加载速度解决单一 bundle 文件过大的方式就可以通过代码分离使用代码分离可以按需加载或者并行加载这些文件分离方式通常有动态导入使用import()这种方式导入多入口起点使用 entry 配置手动分离代码自定义分包Entry Dependencies 或者 SplitChunksPlugin 去重和分离代码1.1. 多入口依赖通过配置对象形式的 entry实现多入口。此时对应的 output.filename 配置项中 filename 需要使用[]的形式来保证每个入口对应一个出口module.export { entry: { main: ./src/main.js, index: ./src/index.js }, output: { filename: [name].bundle.js, path: reslove(__dirname, ./dist), } }使用多入口的弊端如果不同入口的文件依赖了相同的库或者工具函数这些内容将会被各自打包重复解决方法通过额外配置 shared 属性表明共享的模块将每个入口变为对象形式并且增加 dependOn 选项module.exports { entry: { index: { import: ./src/index.js, dependOn: shared }, main: { import: ./src/main.js, dependOn: shared }, shared: [axios] } }最终生成的打包结果中 index 中将会引入三个 bundle1.2. 动态导入动态导入允许在代码执行过程中按需加载特定的模块只有当模块被真正用到时相关的代码才会被加载和执行。有两种动态导入的方式使用import()函数语法导出的内容通过 import.then 中res.default()来获取使用 Webpack 已弃用的 require.ensureindex.js文件中通过import()动态引入 JS 文件const button document.createElement(button); document.body.appendChild(button); button.onclick () { import(./router/about); }动态导入的模块会被单独打包到一个文件中且 HTML 文件中是不会引入 src_router_about_js_bundle.js 的⚡包名称对于动态导入文件最终打包出来的名称默认使用 filename 中设置名称规则。如果想对动态单独生成的包文件进行命名可以在 output 中配置额外的 chunkFilename 来进行定义module.export { output: { clean: true, path: resolve(__dirname, ./dist), filename: [name]-bundle.js, // 只针对分包的文件命名默认情况下获取到的 id 与 name 是一致的 chunkFilename: [id]_[name]_chunk.js } }如果想要自定义名称则需要使用魔法注释/* webpackChunkName: */进行命名button.onclick () { import(/* webpackChunkName: About */ ./router/about); }1.3. SplitChunks第三种分包模式是 SplitChunks底层使用了 SplitChunksPlugin 实现在 Webpack5 中该插件已默认安装默认情况下 SplitChunksPlugin 的默认值 async 只针对import()进行分包。但所使用的第三方库例如 axios 即便导入和使用了其库本身也会被放入主包