Skip to content

可视化大屏的优化方向

约 5174 字大约 17 分钟

2025-06-26

按需导入

按需导入其实就是对引入的代码进行精简,主要涉及到 UI 组件、公共组件、公共方法等一些第三方库、内部封装代码等。

精简内部代码的引入

我们按照模块组织代码,然后通过按需引入,只引入需要的部分。

import { DashBoard } from "./compoent";
import { getOptions } from "./utils";

精简第三方库

Echarts

按需引入 ECharts 模块,只引入需要的模块,避免引入整个 ECharts 库,减少代码体积。

比如我们只涉及到柱状图:

UI 组件

我们可以按需引入页面用到的 UI 组件,从而避免整个组件库被打包进去,从而减少加载的文件体积。因此要求 UI 组件库要支持按需引入,常见的 UI 组件库主要有:element-plusmuiant-design

// 例如只引入下拉框和按钮
import { ElSelect, ElButton } from "element-plus";

其他库

比如前端常用的 lodash

// 引入深拷贝方法
import { cloneDeep } from "lodash";

babel-plugin-import

babel-plugin-import是一个基于 webpack 的 babel 插件,其核心用途是 实现组件和样式的按需加载,从而显著减少打包体积。

一般我们引入组件、第三方库可能像这样以按需引入的方式去写:

import { Button, Select } from "element-ui";

Vue.use(Button);
Vue.use(Select);

但是实际上呢,这个按需引入可能并不是真正的按需引入,甚至可能只是看起来是按需引入。内部依旧可能会有全量引入的情况。

而使用了 babel-plugin-import 插件后,Babel 会将上边的代码转换为类似下面的形式,彻彻底底的变成按需引入:

import Button from "element-ui/lib/button"; // 仅加载Button组件
import "element-ui/lib/button/style.css"; // 自动引入Button样式

import Select from "element-ui/lib/select"; // 仅加载Select组件
import "element-ui/lib/select/style.css"; // 自动引入Select样式

babel-plugin-import 插件介绍

按需加载

React 的 lazy() + <Suspense>

React 的 <Suspense> 是用于管理异步组件加载状态的组件,通常与 React.lazy 搭配使用,实现代码分割和动态加载优化。

我们通过 fallback 属性实现加载过程中的占位内容同,或者异步加载失败显示错误内容。

import React from "react";
const LazyComponent = React.lazy(() => import("./LazyComponent"));
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Vue 的 <Suspense>

Vue3 中的 Suspense 组件是一种特殊的组件,用于处理 适用于异步组件、异步数据请求和其他延迟加载的场景。

Vue 的 <Suspense> 组件

路由的懒加载

前端的路由懒加载主要指的是 ESM 的 import() 动态导入。当用户访问该路由时,才动态加载对应的代码,减少初始的加载时间。

路由懒加载

延迟加载

prefetchpreloadpreconnect

可以利用 prefetchpreloadpreconnect 等 HTTP 头部来预加载资源,提前发起请求,减少延迟。

  • prefetch:浏览器空闲时间,加载未来可能需要的资源(如下一页内容),不阻塞当前页面渲染。
  • preload:优先加载当前页面关键资源(如首屏字体/样式)。
  • preconnect:提前建立与第三方域的 DNS/TLS/TCP 连接(如 CDN 域名),减少后续请求的建立连接的延迟时间。

图片的懒加载

loading="lazy"

可以考虑 lazy 属性实现图片的懒加载。当页面滚动到图片位置时,或者图片进入视口时,浏览器才会加载图片。

<img loading="lazy" src="large-image.jpg" alt="Large Image" />

IntersectionObserver API

也可以考虑 IntersectionObserver 来实现图片的懒加载。

const images = document.querySelectorAll("img");

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

images.forEach((img) => {
  observer.observe(img);
});

滚动加载

滚动加载指的是,通过监听用户滚动行为自动加载内容,当页面滚动到底部的时候,再触发加载对应区域的数据。一般用于流式列表、图文信息流、长表格等。

滚动加载一般通过 IntersectionObserver API 来实现。

JS 的阻塞性处理 defer/async

浏览器在渲染页面时,如果遇到 js 脚本,浏览器会停止渲染等待 js 脚本加载并执行完成,再继续渲染页面,这被称为 JS 的阻塞性

为了解决这个问题,我们可以考虑使用 deferasync 属性。

  • async:不会阻塞,异步加载,加载完成立即执行,但是执行时机不确定。
  • defer:不会阻塞,异步加载,加载完不会立即执行,而是等页面 DOM 结构解析完成后执行,执行顺序会按照加载顺序依次执行。
  • 不加:浏览器会停止渲染,等待该脚本的加载并运行完毕后,再继续加载。

浏览器默认会对带有 type="module" 的脚本使用 defer 属性。

很多框架或工具对构建生成的 js 脚本使用 defer 属性,对动态引入 import() 的组件对应的 js 使用 async 属性,我们一般不用去修改。

如果你使用原生 js 开发的话,可以根据实际情况去处理,关键决策因素为 DOM 要不要停下来等待 JS 的加载执行。

为什么很多框架选择 defer 而不是 async

  1. 保证执行顺序
    • defer 会按 HTML 中的顺序依次执行(chunk-vendors.jsapp.js),确保不会因为依赖关系导致加载报错
    • async 不能保证顺序,可能导致依赖未加载时就执行应用代码,引发错误。
  2. 优化首屏渲染
    • defer 脚本在 DOM 解析完成后按顺序执行,不会阻塞渲染。
    • async 可能在 DOM 解析中途执行,可能阻塞渲染。

代码拆分

多入口拆分

我们首先应该考虑是,可视化大屏能否单独拆分成一个入口。在大多数情况下,可视化大屏都是单独的一个页面。他不包含整体布局、管理等模块,是可以被单独拆分出来的。

同样,新入口对应的 是一个 html 文件,相关 Js、Css、国际化及其他静态资源文件也需要拆分出来,这样对应页面需要加载的代码就大大的减少了:

关键 CSS

构建页面的渲染树需要 CSS,而关键 CSS 指的是首屏加载过程中所必需的 CSS 代码。

这部分代码往往体积比较小,加载时间短,可以通过 内联优先加载 这些 css 资源来加速页面呈现,提升用户体验。

因此我们可以将这部分代码提取出来,内联到 html 中。主要的技术手段有两种:

  1. 手动提取:借助工具分析,手动提取关键 CSS 代码。
  2. 自动提取:借助构建工具插件 critical 自动分析并提取关键 CSS 代码。

参考《关键 CSS 提取》

构建工具的拆分策略配置

构建工具中代码拆分主要指的是,构建工具按照一定的规则自动拆分代码,并将代码打包成多个的 bundle,而不是一个巨大的文件。通过对项目和构建产物的分析,我们可以配置一定的策略来拆分代码,避免出现过大的文件,也同时避免出现过多的小文件,二者都影响页面的加载。

  • 自动拆分:构建工具按照一定的全局规则自动拆分代码,例如:大小,被引用次数等指标。
  • 手动拆分:针对一些较大的第三库、ui 组件、公共方法、布局组件等单独拆分打包。
  • css 的提取:将 CSS 提取出来打包成单独的 css 文件,避免页面文件体积过大,同时可以配置 critical 插件自动提取关键 CSS 代码重新内联到页面中。

图表库优化

选择轻量化图表库

可以考虑一些更加轻量化的图表库,如 Chart.js,功能比较简单,优势在于占用的空间比较小。

new Chart(ctx, {
  type: "bar",
  data: {
    labels: ["Jan", "Feb"],
    datasets: [
      {
        data: [10, 20],
        backgroundColor: "#3498db",
      },
    ],
  },
});

CanvasRender vs SVGRender

适合 CanvasRender 的场景

  • 数据量巨大: 如万级以上的散点图
  • 高频动态更新: 如实时监控仪表盘
  • 需要复杂视觉、动画效果: 如 3D、WebGL 混合渲染
  • 移动端性能敏感场景: 减少 DOM 压力

适合 SVGRender 的场景

  • 需要矢量无损缩放: 如高精度地图
  • 依赖 DOM 交互: 如复杂的图元点击检测
  • 导出矢量图: 如 PDF/SVG 格式报表
  • SEO 友好需求: SVG 内容可被爬虫解析
  • 大量简单图表: 如几十个内容比较简单的图表

分辨率适配

大多数情况下,比较推荐媒体查询和 vw、vh 方案

媒体查询 @media

优先搭配 flex、grid 布局

一般情况下,我们可以根据屏幕的宽度、高度、方向来设计不同的设计稿,然后使用 flex、grid 实现整体的自适应布局。

具体到字号、边距等,可以根据不同的尺寸逐一设置,也可以搭配使用 px、rem、vh、vw 等 CSS 尺寸解决方案来实现多种不同尺寸下的适配。

vhvw 方案

等比适配 vhvw 可以实现等比适配,基于设计稿的基准宽度和高度,两侧及顶部不会留白。但是需要设备和浏览器支持 css3

使用 sass 等 css 预处理器 来实现

// 定义一个设计稿的基准宽度 和 高度
@function px2vw($px, $baseWidth) {
  @return calc($px / $baseWidth) * 100vw;
}
@function px2vh($px, $baseHeight) {
  @return calc($px / $baseHeight) * 100vh;
}
@media screen and (min-width: 1024px) {
  $width: 1024px;
  $height: 768px;
  .test1 {
    border: 1px solid;
    width: px2vw(200px, $width);
    height: px2vh(100px, $height);
  }
}
@media screen and (min-width: 1200px) {
  $width: 1200px;
  $height: 900px;
  .test1 {
    border: 1px solid;
    width: px2vw(200px, $width);
    height: px2vh(100px, $height);
  }
}

编译完

@media screen and (min-width: 1200px) {
  .test1 {
    border: 1px solid;
    width: 16.6666666667vw;
    height: 11.1111111111vh;
  }
}
@media screen and (min-width: 1024px) {
  .test1 {
    border: 1px solid;
    width: 19.53125vw;
    height: 13.0208333333vh;
  }
}

postcss 插件 postcss-px-to-viewport-8-plugin

postcss 插件只能解决 css 里的尺寸转换,无法解决 JS 里的尺寸转换。

postcss.config.js

js 侧的 vh、vw 转换

主要用于 Echart 等图标库数据源里的 px 转换。虽然图标内部也有类似百分比的解决方案,但是为了统一尺寸单位,保证一致的视觉效果,我们还是需要使用 vh、vw 进行转换。

let baseWidth = 1024;
let baseHeight = 768;
function px2vw(px) {
  return (px / baseWidth) * 100 + "vw";
}
function px2vh(px) {
  return (px / baseHeight) * 100 + "vh";
}

Rem 方案

rem 是相对于根元素的字体大小,因此我们可以使用 rem 来实现一个分辨率适配。但是逐渐被 vw/vh 方案取代。

优缺点

  1. 优点:
    1. 兼容性好,支持老版本的浏览器和设备
    2. 等比缩放,不会导致整体比例失调变化
  2. 缺点:
    1. 比较依赖 JS,可能影响首屏渲染
    2. 无法完美适配所有场景:比如大屏场景下,字体过大,同时想显示更多的内容。
    3. 字体大小,容易在大屏上字号过大,小屏上字号过小,影响体验。
    4. 逐渐被 vw/vh 方案取代

flexiable.js

flexible.js 是淘宝团队提出的一种 Rem 方案,它的原理是根据设备的屏幕宽度,动态修改根元素的字体大小,从而实现响应式布局。优点是

这是一种通过动态修改 html 元素的 font-size 属性,来实现 rem 适配的方案。

px2rem

我们也可以使用一些 postCss 的插件,比如 postcss-pxtorem:

// postcss.config.js 示例
module.exports = {
  plugins: {
    "postcss-pxtorem": {
      rootValue: 75, // 750px 设计稿 → 1rem = 75px (750/10)
      propList: ["*"], // 转换所有属性的 px 值
    },
  },
};

CSS scale 缩放方案

通过 CSS transform: scale() 属性动态缩放容器,保持内容比例不变,适合固定尺寸的页面(如大屏数据可视化)。

在使用过程中存在两大缺点:

  • 热区偏移;由于缩放只是视觉上缩放,但是元素布局、大小、尺寸没有跟着变化,可能会导致事件热区偏移,导致交互事件混乱。
  • 两侧留白:当屏幕的宽高比例和设计稿不一致时,就容易出现两侧,或者顶部留白的情况。

Viewport 视口缩放

通过动态修改 <meta name="viewport">initial-scale,让浏览器自动缩放页面。

  • ✅ 原生支持:浏览器自动处理缩放。
  • ❌ 影响所有元素:可能导致字体和边框粗细不一致。
<meta
  name="viewport"
  id="viewportMeta"
  content="width=device-width, initial-scale=1.0"
/>
<script>
  function setViewportScale() {
    const designWidth = 1920; // 设计稿宽度
    const currentWidth = window.innerWidth;
    const scale = currentWidth / designWidth;

    const meta = document.getElementById("viewportMeta");
    meta.content = `width=${designWidth}, initial-scale=${scale}`;
  }

  window.addEventListener("resize", setViewportScale);
  setViewportScale(); // 初始化
</script>

代码压缩

压缩算法

各自特点横向对比适应场景
  • Gzip
  • Brotli
  • zstand

压缩方案

如何配置如何选择
  • 前端预压缩
  • 服务端实时压缩
  • 服务器实时压缩的进阶方案:渐进式压缩。
  • CDN 动态压缩

压缩阈值和等级选择

为什么这么配置?
  • < 10kb:不压缩
  • 10kb - 50kb:压缩、解压缩效率对比传输效率,不明显,根据实际情况配置。
  • > 50kb: 压缩比拉最高 gzip 9brotli 11

缓存优化

强缓存 vs 协商缓存

强缓存 是指浏览器在请求资源时,先检查本地缓存是否有效。如果缓存有效,则直接使用缓存资源,不会向服务器发送请求。

协商缓存 是指浏览器在强缓存失效后,向服务器发送请求,服务器根据请求头判断资源是否更新。如果资源未更新,则返回 304 状态码,浏览器使用本地缓存;如果资源已更新,则返回 200 状态码和新资源。

强缓存与协商缓存的区别

优化实践

可以使用强缓存标记不容易变化的静态资源,如 JS、css、图片等。使用协商缓存标记变化的资源,如 HTML。

在构建过程中,我们可以调整缓存的文件指纹生成策略,尽量降低缓存的变化,提高缓存命中率。

DOM 操作相关

集中操作,避免边读边写

  • DOM 的读取是同步操作,浏览器是立马执行的,甚至为了保证数据的一致性,浏览器需要重新渲染,重新计算最新值。
  • DOM 的修改是异步操作,浏览器不是立马就会执行的,会放到任务队列中等待执行,甚至多个 DOM 修改一起执行。

而边读边写就会破坏这种机制,浏览器就得不得不花费时间去重新渲染,重新计算最新值,这就是为什么我们在操作 DOM 时,要避免边读边写的情况。

我们在处理基于 Dom 实现的动画时,很容易涉及到这些场景,需要我们提前计算,提前存储相关值。

加载优化

Http 2 多路复用

在服务器端开启 Http 2 多路复用,可以利用其多路复用、减少 TCP 连接开销、减少请求延迟等优势。

内存优化

及时销毁图表,释放内存

在 ECharts 中,销毁图表是一个重要的操作。当不再需要某个图表实例时,应该及时销毁它,以释放内存和资源。

  • 释放内存:清除图表实例、事件监听、定时器等,避免内存泄漏。
  • 移除 DOM:删除图表生成的 canvas 或 svg 节点。
  • 解除引用:断开与 ECharts 容器的关联。
// 销毁图表实例
myChart.dispose();

接口优化

大量待计算的数据

如果遇到接口返回的是大量数据,前端的计算比较复杂,可以考虑以下优化方案:

  • 利用 webwork 来进行计算
  • 后端定时任务,定期把计算好的数据存到表里,前端请求时,直接返回最新的计算结果。

大量的接口请求

可以考虑将多个接口请求合并为一个,减少接口请求的次数。

防呆设计

loading 状态

在接口请求期间,我们可以先显示一个 loading 状态,等数据返回后再刷新图表。

数据为空

毕竟接口需要时间才能返回数据,我们可以先按照一定的格式先把图表渲染拿出来,显示空数据,等拿到数据再刷新进去。

缓存请求结果

如果部分接口比较慢,可以考虑提前渲染上一次请求的结果,并在本次请求结束后重新刷新数据,并缓存本次的数据用于下一次请求前的渲染。

骨架屏

正式结果出来之前,我们可以先渲染一个骨架屏,骨架屏的样式可以参考 loading 状态的样式。

错误处理

如果接口请求失败,我们可以先显示一个错误提示,等用户点击后再重新请求数据。

垂直方向滚动条

如果视口内元素过多,尽量不要让页面产生横向滚动条。尽量引导用户在纵向上滚动页面查看更多内容。

数据下钻

如果用户点击了某个元素,我们可以通过下钻的方式,来展示更多的信息。

更新日志

2025/8/24 08:17
查看所有更新日志
  • e7112-1