Frontend Development 17 min read

Fit @galacean/effects into a 2 MB WeChat Mini‑Program with Async Package Splitting

To overcome the 2 MB main‑package limit of WeChat mini‑programs, this guide details a step‑by‑step solution that uses @galacean/effects for stunning homepage animations, applies page and module splitting, Babel and webpack plugins, and Taro configuration to move large code into asynchronous sub‑packages.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Fit @galacean/effects into a 2 MB WeChat Mini‑Program with Async Package Splitting

Background

The Guming membership system upgrade requires a cool animation on the mini‑program homepage. Using @galacean/effects results in a compiled size of 599.3KB, but the main package limit is only 2MB, making it impossible to fit.

Current optimizations applied in the Guming ordering mini‑program include:

Page sub‑packages: only the default launch page/TabBar page remain in the main package; other pages are moved to sub‑packages.

Image resource optimization: upload local resources and replace them with network URLs via a Babel plugin.

Common module sub‑packaging: configure Taro's

mini.optimizeMainPackage

to pack unused common modules into corresponding sub‑packages.

All optimizations are done, but the build still exceeds the size limit, and there is no way to request extra MB from WeChat.

Goals

Use

@galacean/effects

to display a cool animation on the mini‑program homepage while keeping the main package under 2MB.

Establish a generic solution to reduce the size impact of other third‑party dependencies or business components in the main package.

Implementation

Since the main package cannot hold the large code, we move it to sub‑packages using "sub‑package async loading".

Core ideas:

Split large JS code into sub‑packages.

Leverage WeChat's sub‑package async capability to reference code across sub‑packages.

Implementation V1 (Babel)

Implementation Idea

Define a global marker function

asyncRequire

to tag async modules.

Babel plugin detects

asyncRequire

calls, collects async modules, and replaces them with cross‑sub‑package references.

Webpack plugin (using esbuild) builds the async packages.

Code

<code>/**
 * Async load third‑party dependency
 */
declare const asyncRequire: <T = any>(packagePath: string) => Promise<T>;</code>
<code>export const promiseRetry = async (fn: () => Promise<any>, retries = 3, delay = 1000) => {
  try {
    return await fn();
  } catch (error) {
    if (retries <= 0) return Promise.reject(error);
    await new Promise(resolve => setTimeout(resolve, delay));
    return promiseRetry(fn, retries - 1, delay);
  }
};</code>
<code>const asyncPackagePaths = new Set();
const generateAsyncPackageName = (packagePath) => packagePath.replace(/\//g, '-');
// ...webpack plugin that builds each async package and updates app.json
</code>

Implementation V2 (SplitChunk)

V1 meets the animation goal, but the broader goal of a reusable solution remains. Issues include handling component styles/images, keeping Babel plugin configurations consistent, and maintaining path aliases.

Solutions:

Use dynamic

import('modulePath')

for async module loading.

Configure

splitChunk

to split async modules into designated sub‑packages.

Modify webpack runtime to enable cross‑sub‑package JS references (webpack normally creates a

Script

for async modules).

Use Taro to modify the mini‑program configuration and register the sub‑packages.

Code

<code>module.exports = {
  presets: [
    ['taro', {
      framework: 'react',
      ts: true,
      loose: false,
      useBuiltIns: false,
      "dynamic-import-node": process.env.TARO_ENV !== 'weapp',
      targets: { ios: '9', android: '5' }
    }]
  ]
};</code>
<code>chain.optimization.merge({
  splitChunks: {
    cacheGroups: {
      common: { name: 'common', minChunks: 2, priority: 1, enforce: true },
      vendors: { name: 'vendors', minChunks: 2, test: m => /[\\/]node_modules[\\/]/.test(m.resource), priority: 10, enforce: true },
      [`${finalOpts.dynamicModuleJsDir}-js`]: {
        name: m => `${finalOpts.dynamicModuleJsDir}/${m.buildInfo.hash}`,
        chunks: 'async',
        test: /\.(js|jsx|ts|tsx)$/,
        enforce: true
      },
      // ...other cache groups for CSS and common async JS
    }
  }
});</code>

Webpack Runtime Transform

<code>import { parse } from '@babel/parser';
import generate from '@babel/generator';
import traverse from '@babel/traverse';
import * as types from '@babel/types';

export const replaceWebpackRuntime = (code, opts) => {
  const ast = parse(code);
  traverse(ast, {
    AssignmentExpression(path) { /* replace __webpack_require__.l implementation */ },
    VariableDeclarator(path) { /* replace loadStylesheet, remove create/findStylesheet */ }
  });
  return generate(ast).code;
};</code>

Additional Plugins

Two webpack plugins are added before compression:

TransformBeforeCompression

runs

replaceWebpackRuntime

on the runtime file.

RequireStylesheet

appends an

@import

of the generated async CSS to the main

app.wxss

.

<code>class TransformBeforeCompression {
  apply(compiler) {
    compiler.hooks.compilation.tap('TransformBeforeCompression', compilation => {
      compilation.hooks.processAssets.tap({ name: 'TransformBeforeCompression', stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE }, assets => {
        for (const name of Object.keys(assets)) {
          if (!/runtime\.js$/.test(name)) continue;
          const src = assets[name].source();
          const transformed = replaceWebpackRuntime(src, { /* opts */ });
          compilation.updateAsset(name, new compiler.webpack.sources.RawSource(transformed));
        }
      });
    });
  }
}

class RequireStylesheet {
  apply(compiler) {
    compiler.hooks.compilation.tap('RequireStylesheet', compilation => {
      compilation.hooks.processAssets.tap({ name: 'RequireStylesheet', stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE }, assets => {
        for (const name of Object.keys(assets)) {
          if (!/app\.wxss$/.test(name)) continue;
          const src = assets[name].source();
          const newSrc = src + "@import './dynamic-common.wxss';";
          compilation.updateAsset(name, new compiler.webpack.sources.RawSource(newSrc));
        }
      });
    });
  }
}
</code>

Usage in Business Code

Animation component loads the compiled effect library asynchronously:

<code>import Taro from '@tarojs/taro';
import React, { useEffect, useRef } from 'react';

export const Animation: React.FC = async () => {
  const animationRef = useRef();
  const initAnimation = async () => {
    animationRef.current = await asyncRequire('@/assets/libs/mp-weapp-galacean-effects');
    // ...initialize animation
  };
  useEffect(() => { initAnimation(); }, []);
  return <Canvas type="webgl" id="webglCanvas" width={`${systemInfo.screenWidth}`} height={`${systemInfo.screenHeight}`} />;
};
</code>

Component and function lazy‑loading examples using React

lazy

and dynamic

import

are also provided.

Conclusion

Successfully fit

@galacean/effects

into the mini‑program, displaying a cool membership upgrade animation on the homepage while staying under the 2 MB limit.

Demo GIF
Demo GIF
reactBabelPackage OptimizationWeChat Mini ProgramwebpackTaroAsync Loading
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.