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.optimizeMainPackageto 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/effectsto 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
asyncRequireto tag async modules.
Babel plugin detects
asyncRequirecalls, 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
splitChunkto split async modules into designated sub‑packages.
Modify webpack runtime to enable cross‑sub‑package JS references (webpack normally creates a
Scriptfor 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:
TransformBeforeCompressionruns
replaceWebpackRuntimeon the runtime file.
RequireStylesheetappends an
@importof 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
lazyand dynamic
importare also provided.
Conclusion
Successfully fit
@galacean/effectsinto the mini‑program, displaying a cool membership upgrade animation on the homepage while staying under the 2 MB limit.
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.