性能优化学习

https://developer.chrome.com/docs/devtools/performance/selector-stats?hl=zh-cn

在 Chrome Performance 面板里,想看 Layout(重排 / 回流) 非常直观,我给你用最简单的方式讲清楚怎么看、怎么定位。

一、先打开 Performance

  1. F12 → Performance
  2. 点左上角 ● 录制
  3. 刷新页面 / 操作页面
  4. 点 ■ 停止,等待分析结果出来

二、怎么识别 Layout

主要看这 3 个地方:

1. 主面板颜色条(最直观)

  • 蓝色条:Loading(加载、解析 HTML/CSS)
  • 黄色条:Scripting(JS 执行)
  • 紫色条:Rendering → 这里面就包含 Layout
  • 绿色条:Painting(重绘)
  • 灰色条:System / Idle

只要看到 紫色条特别长、特别多、特别碎,就是 Layout 太多、性能差。

2. 看 Main 线程任务里的标签

点开 Main 线程的火焰图:

  • 看到 Layout 字样 → 就是重排
  • 看到 Forced Synchronous Layout → 强制同步布局(最伤性能)
  • 看到 Recalculate Style → 重新计算样式(一般会跟着 Layout)

3. 看 Summary 统计

底部 Summary 面板:

  • Rendering 时间占比高 → Layout / 样式计算多

三、重点:怎么看 "哪个 JS 导致了 Layout"

  1. 找到紫色的 Layout 任务
  2. 点开右边 Call Stack(调用栈)
  3. 栈顶就是触发 Layout 的 JS 代码位置
  4. 点文件名,直接跳到源码

常见触发:

  • 读写 offsetWidth /clientHeight/getBoundingClientRect
  • 频繁改 style(width/height/top/left)
  • 频繁增删 DOM
  • 窗口 resize / 滚动

四、Forced Synchronous Layout(强制同步布局)

这是性能杀手,表现为:

  • 火焰图里出现 密集小紫条
  • 任务名标红:Forced Synchronous Layout

原因:JS 一边读布局属性 ,一边改样式,浏览器被迫不停重排。

五、你关心 ECharts 多图表场景怎么看

多 ECharts 刷新时,你重点看:

  1. 是不是一堆 密集紫色 Layout 条
  2. 是不是有 Forced Synchronous Layout
  3. ECharts 容器是否频繁触发尺寸计算(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-changetransform: translateZ(0)主动提升元素为独立图层,减少重绘范围

transform的优势:完全在合成层执行,不触发重排 / 重绘,是动画性能最优方案

合成层由 GPU 管理,过多独立图层会增加内存开销,需合理控制图层数量

合浏览器渲染原理和 ECharts 特性,我们可以通过主动提升合成层 +合理使用 GPU 加速属性,大幅降低多图表页面刷新 / 渲染时的重排、重绘开销。


一、核心原理回顾

ECharts 的canvas渲染本质上是绘制在 DOM 元素上的位图,默认情况下,这些 canvas 会和页面其他元素共用合成层,每次刷新 / 重绘都会触发全页面的 Paint+Composite。通过以下方式,我们可以让图表容器独立为合成层,让后续的动画 / 更新仅触发 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;
}

/* 方案2:will-change 提前告知浏览器(适合已知会频繁更新的图表) */
.echarts-container {
  will-change: transform, opacity;
}

<!-- 每个图表容器都应用该样式 -->
<div class="echarts-container" id="chart1"></div>
<div class="echarts-container" id="chart2"></div>

2. 页面刷新 / 初始化时的性能优化

(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);
}

如果需要响应式适配,优先使用 CSS transform: 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;
}
(3)filter 属性的 GPU 优化使用

部分filter滤镜(如blur)可在 GPU 层执行,适合图表加载时的占位效果:

复制代码
.echarts-loading {
  transform: translateZ(0);
  filter: blur(2px); /* GPU层执行模糊,不触发布局重排 */
  opacity: 0.7;
}

三、进阶优化:合成层管理与避坑

1. 避免合成层爆炸

  • 不要给所有元素都加transform: translateZ(0),过多合成层会增加 GPU 内存开销
  • 仅给频繁更新 / 动画的图表容器提升合成层,静态图表无需额外处理

2. 合成层优化的验证方法

使用 Chrome DevTools 查看合成层:

  1. 打开控制台 → More toolsLayers
  2. 查看每个图表容器是否被标记为独立合成层(Composited Layers
  3. 检查重绘区域:使用Rendering面板的Paint flashing,确认图表更新时仅自身区域重绘

3. 兼容性说明

  • transform: translateZ(0) 兼容性:所有现代浏览器均支持,移动端也兼容
  • will-change:Chrome/FF 支持,旧版浏览器会忽略,不影响基础功能
  • filter:部分浏览器对filter的 GPU 加速支持有限,优先用于非关键路径

四、额外的 ECharts 性能优化(配合合成层效果翻倍)

  1. 渲染器选择 :大数据量场景优先使用renderer: 'canvas',避免 SVG 生成大量 DOM 节点

    复制代码
    const chart = echarts.init(dom, null, { renderer: 'canvas' });
  2. 数据降采样 :使用sampling减少渲染数据点,降低 canvas 绘制开销

    复制代码
    option = {
      series: [{
        type: 'line',
        data: largeData,
        sampling: 'lttb' // 大数据采样优化,保留关键拐点
      }]
    };
  3. 关闭非必要动画 :禁用初始渲染和更新动画,减少主线程计算

    复制代码
    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 class="echarts-container" id="chart1"></div>
<div class="echarts-container" id="chart2"></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 文件过大的方式就可以通过代码分离,使用代码分离可以按需加载或者并行加载这些文件,分离方式通常有:

  1. 动态导入:使用import()这种方式导入
  2. 多入口起点:使用 entry 配置手动分离代码
  3. 自定义分包: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"),
    }
}

使用多入口的弊端:

如果不同入口的文件依赖了相同的库或者工具函数,这些内容将会被各自打包(重复),解决方法:

  1. 通过额外配置 shared 属性表明共享的模块

  2. 将每个入口变为对象形式,并且增加 dependOn 选项

    module.exports = {
    entry: {
    index: {
    import: "./src/index.js",
    dependOn: "shared"
    },
    main: {
    import: "./src/main.js",
    dependOn: "shared"
    },
    shared: ["axios"]
    }
    }

最终生成的打包结果中 index 中将会引入三个 bundle:

1.2. 动态导入

动态导入允许在代码执行过程中按需加载特定的模块,只有当模块被真正用到时,相关的代码才会被加载和执行。有两种动态导入的方式:

  1. 使用import()函数语法(导出的内容通过 import.then 中 res.default() 来获取)
  2. 使用 Webpack 已弃用的 require.ensure

index.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 即便导入和使用了,其库本身也会被放入主包

相关推荐
浮槎来2 小时前
光伏组件的PID学习
运维·学习·硬件工程·光伏
绿豆人2 小时前
RPC项目学习2
网络协议·学习·rpc
码喽7号2 小时前
vue学习五:前端路由VueRouter
前端·vue.js·学习
_李小白2 小时前
【OSG学习笔记】Day 49: 实战鼠标拾取与高亮显示
笔记·学习·计算机外设
何如呢3 小时前
FIFO的IP核学习
学习·fpga开发
折锦烟3 小时前
AI Agent 开发 0-1 学习路线(学习目标)
学习
艾莉丝努力练剑3 小时前
【Linux线程】Linux系统多线程(六):<线程同步与互斥>线程同步(上)
java·linux·运维·服务器·c++·学习·线程
brave_zhao3 小时前
什么是增值税
学习
herinspace3 小时前
管家婆实用帖-如何使用ping命令检测网络环境
网络·数据库·人工智能·学习·excel·语音识别