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.
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
/**
* Async load third‑party dependency
*/
declare const asyncRequire: <T = any>(packagePath: string) => Promise<T>; 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);
}
}; const asyncPackagePaths = new Set();
const generateAsyncPackageName = (packagePath) => packagePath.replace(/\//g, '-');
// ...webpack plugin that builds each async package and updates app.jsonImplementation 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
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' }
}]
]
}; 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
}
}
});Webpack Runtime Transform
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;
};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.
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));
}
});
});
}
}Usage in Business Code
Animation component loads the compiled effect library asynchronously:
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}`} />;
};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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
