微信小程序 ECharts 瘦身实战:分包异步化 + componentPlaceholder 避开主包 2MB 限制

微信小程序用分包异步化接入 ECharts

ECharts 的 echarts.js 体积接近 1MB,直接放在主包里,很容易让主包超过 2MB。

普通分包适合按页面拆分,但主包页面不能直接同步使用分包中的组件。需要在页面中配置 componentPlaceholder,先渲染一个占位节点,等 ECharts 分包下载完成后再替换成真实组件。

目录结构

text 复制代码
miniprogram/
├─ app.json
├─ pages/
│  └─ chart/
│     ├─ index.json
│     ├─ index.wxml
│     └─ index.scss
└─ subpackages/
   └─ echarts/
      ├─ ec-canvas/
      │  ├─ ec-canvas.js
      │  ├─ ec-canvas.json
      │  ├─ ec-canvas.wxml
      │  ├─ ec-canvas.wxss
      │  ├─ echarts.js
      │  └─ wx-canvas.js
      └─ components/
         └─ echart/
            ├─ index.json
            ├─ index.ts
            ├─ index.wxml
            └─ index.scss

echarts.jsec-canvas 和包装组件都放在同一个分包中。主包页面只使用包装组件,不要再直接导入 ECharts。

声明 ECharts 分包

app.json 中增加一个只提供组件的分包:

json 复制代码
{
  "pages": [
    "pages/chart/index"
  ],
  "subPackages": [
    {
      "root": "subpackages/echarts",
      "pages": []
    }
  ],
  "lazyCodeLoading": "requiredComponents"
}

分包本身仍受单包 2MB 限制。如果完整 ECharts 构建超过限制,需要使用 ECharts 自定义构建,只保留项目实际用到的图表和组件。

在分包内包装 ec-canvas

subpackages/echarts/components/echart/index.json

json 复制代码
{
  "component": true,
  "styleIsolation": "apply-shared",
  "usingComponents": {
    "ec-canvas": "/subpackages/echarts/ec-canvas/ec-canvas"
  }
}

index.wxml

xml 复制代码
<view class="chart-shell">
  <ec-canvas
    id="echart-canvas"
    class="chart"
    canvas-id="{{canvasId}}"
    ec="{{ec}}"
  />
</view>

index.ts

ts 复制代码
import * as echarts from '../../ec-canvas/echarts';
import type { EChartsInstance } from '../../ec-canvas/echarts';

interface EChartsCanvas {
  setChart(chart: EChartsInstance): void;
}

interface EcCanvasComponent {
  init(
    callback: (canvas: EChartsCanvas, width: number, height: number, devicePixelRatio: number) => EChartsInstance,
  ): void;
}

interface EChartData {
  ec: {
    lazyLoad: boolean;
  };
}

type EChartProperties = Record<string, WechatMiniprogram.Component.AllProperty> & {
  canvasId: {
    type: StringConstructor;
    value: string;
  };
  option: {
    type: ObjectConstructor;
    value: Record<string, unknown>;
  };
};

type EChartMethods = Record<string, Function> & {
  renderChart(): void;
};

const charts = new WeakMap<object, EChartsInstance>();

Component<EChartData, EChartProperties, EChartMethods, []>({
  properties: {
    canvasId: {
      type: String,
      value: 'echart',
    },
    option: {
      type: Object,
      value: {},
    },
  },

  data: {
    ec: {
      lazyLoad: true,
    },
  },

  observers: {
    option(): void {
      this.renderChart();
    },
  },

  lifetimes: {
    ready(): void {
      this.renderChart();
    },
    detached(): void {
      charts.get(this)?.dispose();
      charts.delete(this);
    },
  },

  methods: {
    renderChart(): void {
      const option = this.properties.option;
      if (Object.keys(option).length === 0) return;

      const chart = charts.get(this);
      if (chart) {
        chart.setOption(option, true);
        return;
      }

      const canvasComponent = this.selectComponent('#echart-canvas') as unknown as EcCanvasComponent | null;
      if (!canvasComponent) return;
      canvasComponent.init((canvas, width, height, devicePixelRatio) => {
        const newChart = echarts.init(canvas, null, { width, height, devicePixelRatio });
        charts.set(this, newChart);
        canvas.setChart(newChart);
        newChart.setOption(this.properties.option, true);
        return newChart;
      });
    },
  },
});

这里使用 lazyLoad,由包装组件在 ready 后调用 ec-canvas.initoption 为空时不初始化,数据先于分包加载完成时,真实组件创建后会读取最新的 option

实例保存在 WeakMap 中,每个组件实例分别持有自己的 ECharts 实例。组件销毁时调用 dispose(),同一页面放置多个图表也不会共享实例。

组件自身的 index.scss 负责把 Canvas 高度传递到宿主尺寸:

scss 复制代码
:host,
.chart-shell,
.chart {
  display: block;
  height: 100%;
  width: 100%;
}

主包页面配置 componentPlaceholder

pages/chart/index.json

json 复制代码
{
  "usingComponents": {
    "echart": "/subpackages/echarts/components/echart/index"
  },
  "componentPlaceholder": {
    "echart": "view"
  }
}

usingComponentscomponentPlaceholder 中的组件名必须一致。

页面首次渲染时,如果 ECharts 分包还没有下载,基础库会先把 <echart> 当作 view 渲染。分包可用后,占位 view 会被真实组件自动替换。

页面使用

业务页面只负责生成 ECharts option 并传给组件:

xml 复制代码
<view class="chart-frame">
  <echart
    class="chart"
    canvas-id="history-chart"
    option="{{chartOption}}"
  />
</view>

index.scss

css 复制代码
.chart-frame {
  display: block;
  width: 100%;
  height: 520rpx;
}

.chart {
  display: block;
  width: 100%;
  height: 100%;
}

外层必须提前设置高度。否则占位节点没有尺寸,真实组件替换后,ECharts 初始化得到的 Canvas 高度也可能是 0。

实际加载过程如下:

text 复制代码
主包页面开始渲染
  → 使用 view 代替 echart
  → 异步下载 ECharts 分包
  → 真实组件替换占位 view
  → 组件 ready
  → ec-canvas.init
  → echarts.init
  → setOption

开发者工具中可以通过"代码依赖分析"检查结果:echarts.js 应只存在于 subpackages/echarts,主包中不应再有一份副本。

componentPlaceholder 需要基础库 2.11.2 及以上版本。发布前还需要清除开发者工具缓存,模拟首次进入页面,检查占位切换、Canvas 尺寸和真机渲染是否正常。

参考资料

相关推荐
律宏阔16 分钟前
微信小程序集成 TDesign 完整记录:解决 NPM packages not found
前端·微信小程序
夏天要喝冰可乐26 分钟前
从 Idea 到开源插件:我用 Vibe Coding 做了「文章摆渡」
前端·ai编程·vibecoding
小小小小宇33 分钟前
Pi 手动添加
前端
然我1 小时前
模型不是 Agent:从零实现一个最小 Agent Loop
前端·人工智能·agent
boooooooom1 小时前
手把手做一个图 RAG 烹饪问答系统:Neo4j + Milvus + LLM 的工程实践
前端·javascript·后端
小小善后师1 小时前
HID 设备对接技术解析:基于本地中间服务的 WebSocket 通信模式
前端
黄油面包1 小时前
Codex 额度三天见底后,我重新做了一周预算
前端·人工智能
PedroQue991 小时前
v2.7.1:修复 H5 端返回死循环闪烁问题
前端·uni-app
coderCN1 小时前
Nodejs express+knex(ORM框架)
前端·node.js