本文以具体项目为例,介绍了小程序开发中的几点技术优化。本文项目采用 mpvue 框架,但文中提出的优化技术要点对采用其他框架的小程序项目依然有参考意义。
背景
拼房帝小程序是一款新房信息服务类小程序。拼房帝作为搜狐焦点最重要的 C 端产品,承载了很多业务功能,排除内嵌的 H5 页面以外,已有 150 多个小程序页面。随着功能的迭代,项目变得越来越臃肿,技术层面也面临很多问题。一方面,开发构建越来越耗时,影响开发效率;另一方面,打包体积越来越庞大,对应的启动耗时变长,影响用户体验。
针对以上问题,我们进行了一系列的优化:通过部分编译,提升开发时的编译效率;通过 webpack loader 优化提升打包速度;通过分包依赖优化、静态资源优化减小打包体积......
优化点一:分包依赖优化
分包依赖优化,并不是一个专有名词,而是我根据个人理解定义的,大概意思就是合理进行页面分包划分、将公共模块打包至合理的位置。首先介绍一下小程序的分包机制。
小程序分包机制
微信小程序提供了分包机制,开发者可以根据需要将项目页面进行分包划分。采用分包后,微信会根据用户访问页面按需加载资源,从而优化小程序首次启动的下载时间,详细请阅读官方文档。下面列出几点重要的规则:
- 用户从主包页面打开小程序,只需下载主包资源;
- 用户从分包 A 页面打开小程序时,需下载主包和分包 A资源;
- 分包 A 只能引用分包 A 及主包内资源,无法引用分包 B 内资源;
- 主包不能引用分包内的资源;
- TabBar 页面必须在主包;
- 主包\单个分包限制 2MB, 整包限制 20MB;
怎样打包才更合理
那么,根据以上规则,怎样打包才更合理呢?
- 主包尽量精简,只需包含 TabBar 页面和高频访问页面;
- 如果 module1 同时被主包和分包A引用,则 module1 应被打进主包(毋庸置疑);
- 如果 module1 仅被分包A内的页面引用,则 module1 应打包至分包 A 内部(优化重点);
- 大多数的开发人员可能会认为,分包内只能放分包页面,其实分包目录下可以放任何资源,且可以被分包内的页面引用;
- 也有人认为公共依赖只能放在主包,其实分包内的公共依赖也可以放在分包自身目录下;
- 以上两点大家可以自行测试验证,这也是我们可以优化的关键点;
- 如果 module1 同时被分包 A、B 引用,且没有被主包引用,则可以将 module1 打到主包,也可以打到各个分包(视情况而定);
优化前存在的问题
通过 webpack-bundle-analyzer 分析我们项目可以发现,打包结果中存在两份 tim-wx.js,三份 webim_wx.js,并且 vender.js 中存在多个仅有子包引用的公共模块,这显然是不合理的。
mpvue 使用的 webpack 版本为3.11.0,使用 CommonsChunkPlugin 插件提取公共代码(webpack4 已经用SplitChunksPlugin 替换 CommonsChunkPlugin)。其配置项如下:
// build/webpack.prod.config.js
new webpack.optimize.CommonsChunkPlugin({
name: 'common/vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf('node_modules') >= 0
) || count > 2
}
})
根据以上配置规则,会存在以下问题:
- 如果某一个 npm 模块只在某一个分包页面引用,该模块仍会被打进主包中的 vendor;
- 如果 moduleA 仅在某一个分包内引用大于 2 次,moduleA 仍会被打进主包中的 vendor;
- 如果 moduleA 在某一个分包内引用 2 次,moduleA 会被打进分包内的各个页面,从而造成分包内共用的模块没有被抽离,增加了代码包体积;
如何优化
首先,明确我们要优化的目标:分包内部依赖的公共模块打包至分包目录下,减小主包 common/vendor.js 体积。我们先来看下优化之前 CommonsChunkPlugin 的工作流程:
webpack 以 main.js 和各个页面的 main.js 为打包入口,通过各种 loader 加载资源,然后通过 CommonsChunkPlugin 提取公共模块至主包下的 common/vendor.js。我们的优化主要是修改以上流程,先通过 CommonsChunkPlugin 分别提取各个分包的公共模块至各个分包目录下,然后再提取全局的公共依赖,其流程大致如下:
具体代码为:
// build/utils.js
exports.getSubPackageCommonChunkPlugins = function () {
const appJson = require('../src/app.json')
const subPackages = {}
if (appJson.subPackages) {
appJson.subPackages.forEach(package => {
subPackages[package.root] = []
package.pages.forEach(subPage => subPackages[package.root].push(path.join(package.root, subPage)))
})
}
return Object.keys(subPackages).map(sub =>
new webpack.optimize.CommonsChunkPlugin({
name: path.join(sub, 'common'),
chunks: subPackages[sub],
minChunks: 2,
})
)
}
// build/webpack.prod.config.js
var utils = require('./utils')
var subPackageCommonChunkPlugins = utils.getSubPackageCommonChunkPlugins()
// webpack plugins
plugins: [
...subPackageCommonChunkPlugins,
new webpack.optimize.CommonsChunkPlugin({
name: 'common/vendor',
minChunks: function (module, count) {
// 这里可以针对不同模块做自定义配置,仅做参考
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf('node_modules') >= 0
&& count > 1
) || count > 2
}
})
]
优化效果
通过分包依赖优化,我们将分包内公共模块提取至分包内部,有效减小了主包及各个分包代码体积。
优化点 | 优化前体积(kb) | 优化后体积(kb) | 效果 | |||
---|---|---|---|---|---|---|
主包 | 整包 | 主包 | 整包 | 主包 | 整包 | |
分包依赖优化 | 1940 | 7144 | 1809 | 6523 | -131 | -621 |
优化点二:webpack loader 优化
优化点
- 开启 babel-loader 缓存;
- 检查各个 loader 是否有必要(比如,生产环境是否需要 eslint-loader );
- 优化 loader 处理文件的校验规则(include、exclude);
- 不处理无需处理的文件,如第三方包;
- 缩小文件查询范围,如指定处理 src 目录;
- 删除不需要的校验规则,如对字体文件、音视频文件的校验;
优化效果
优化点 | 优化前耗时 | 优化后耗时 | 效果 | |
---|---|---|---|---|
1 | eslint-loader 不处理第三方依赖 | 122 | 116 | -6 |
2 | 删除生产环境中的 eslint-loader | 116 | 95 | -21 |
3 | 删除 url-loader 对字体文件、音视频文件的校验 | 95 | 94 | -1 |
4 | babel-loader 不处理第三方依赖 | 94 | 84 | -10 |
5 | babel-loader 开启本地缓存 | 84 | 80 | -4 |
注:实验机器为 MacBook Pro 2020(1.4 GHz 四核 i5 / 16 GB ),时间单位为秒。
优化点三:静态资源优化
存在的问题
- 项目前期将较多静态图片放在本地;
- 图片转换为 base64 字符串之后,体积一般会变大(约30%);
- 在 CSS 中同一张本地图片被引用多次,编译打包后会就会存在多份重复的 base64 字符串;
- 垃圾代码:项目 JS 文件中竟然存在 base64 字符串常量来代替图片;
优化
- 图片、字体等静态资源尽量全部上传 CDN,避免静态资源参与打包编译,以提升编译效率、减小代码体积;
- 删除 base64 字符串常量;
优化效果
优化点 | 优化前体积(kb) | 优化后体积(kb) | 效果 | ||||
---|---|---|---|---|---|---|---|
主包 | 整包 | 主包 | 整包 | 主包 | 整包 | ||
1 | 静态资源优化 | 1809 | 6523 | 1607 | 6175 | -202 | -348 |
优化点四:修改 webpack 打包入口
存在的问题
- mpvue 默认 entry 为 main.js 和 /pages/**/main.js,webpack 会将 src/pages/ 目录下所有页面打包;
- 当想下线某一个页面时必须移除该页面对应代码,否则即使没有在 app.json 中配置该页面,最终的打包结果仍会包含该页面;
优化
修改 entry 为 app.json 中配置的页面。具体实现就是遍历 app.json 中的 pages 及 subPackages 中的页面作为打包入口。
// build/webpack.base.conf.js
const rootSrc = path.join(__dirname, '..', './src')
const appJson = require('../src/app.json')
const pages = JSON.parse(JSON.stringify(appJson.pages))
if (appJson.subPackages) {
appJson.subPackages.forEach(subPackage => {
subPackage.pages.forEach(page => {
pages.push(subPackage.root + page)
})
})
}
const pagesEntry = {}
pages.forEach(page => {
pagesEntry[page] = `${rootSrc}/${page}.js`
})
const appEntry = { app: path.join(__dirname, '..', './src/main.js') }
const entry = Object.assign({}, appEntry, pagesEntry)
module.exports = {
entry,
output: {
path: config.build.assetsRoot,
jsonpFunction: 'webpackJsonpMpvue',
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
}
......
}
有何好处
- 更符合小程序自身逻辑;
- 当下线某一个页面时,可以仅修改 app.json 配置文件,而无须删除代码,方便快捷;
优化点五:部分编译
存在的问题
随着项目页面不断增加,项目编译越来越慢,在开发阶段,执行 npm run dev
命令等待时间越来越长,成为开发中的最大痛点,严重影响开发效率和体验。
部分编译
如何快速提升编译效率,有一个最有效、最直接的方法,就是部分编译。何为部分编译,就是仅仅编译开发时需要的页面,而不是让项目全部页面参与编译。我们的整个项目包大约有 150 个页面,而每次开发时可能只涉及几个页面,采用部分编译,可以大大提升编译效率,完美解决开发阶段的编译效率问题。
部分编译的实现也比较简单:
- 新建一个 app.json 文件的副本,重命名为 app.dev.json,只需在 app.dev.json 中配置开发时涉及的几个页面;
- 通过 cross-env 添加环境变量
only
,例如"dev:only": "rimraf ./dist && cross-env only=true node build/dev-server.js "
,当 only 为 true 时,将 webpack 的 entry 修改为 app.dev.json 中配置的页面作为打包入口; - 修改 dev-server 文件,通过 webpack-dev-middleware-hard-disk 中间件将 app.dev.json 中内容复制至 dist 目录下app.json 中。
通过以上修改,执行 npm run dev:only
命令就可以只编译部分页面,从而提高编译速度。
// build/webpack.base.conf.js
const isOnly = process.env.only
const rootSrc = path.join(__dirname, '..', './src')
const appJson = isOnly ? require('../src/app.dev.json') : require('../src/app.json')
const pages = JSON.parse(JSON.stringify(appJson.pages))
if (appJson.subPackages) {
appJson.subPackages.forEach(subPackage => {
subPackage.pages.forEach(page => {
pages.push(subPackage.root + page)
})
})
}
const pagesEntry = {}
pages.forEach(page => {
pagesEntry[page] = `${rootSrc}/${page}.js`
})
const appEntry = { app: path.join(__dirname, '..', './src/main.js') }
const entry = Object.assign({}, appEntry, pagesEntry)
// build/dev-server.js
var webpack = require('webpack')
var webpackConfig = require('./webpack.dev.conf')
var express = require('express')
var app = express()
var compiler = webpack(webpackConfig, function() {
if (process.env.only) {
const appJson = require(path.resolve(__dirname, '../src//app.dev.json'))
fs.writeFileSync(path.resolve(__dirname, '../dist/app.json'), JSON.stringify(appJson))
}
})
module.exports = new Promise((resolve, reject) => {
var server = app.listen('8080', 'localhost')
// for 小程序的文件保存机制
require('webpack-dev-middleware-hard-disk')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
resolve({
ready: Promise.resolve(),
close: () => {
server.close()
}
})
})
其他优化点
- 检查第三方依赖包是否可以精简,比如 echarts 可以采用自定义版本,删除不需要的绘图功能;
- 检查打包后是否有冗余文:小程序会将dist目录下所有文件打包上传,应删除冗余文件;
- 检查 statis 目录,删除未引用文件;
总结
通过分包依赖优化、静态资源优化,将打包体积减小约 13%;通过 webpack loader 优化,将打包速度提升约 40%;通过修改 webpack 打包入口、部分编译,有效提升开发效率。
优化点 | 优化前耗时 | 优化后耗时 | 效果 | |
---|---|---|---|---|
1 | eslint-loader 不处理第三方依赖 | 122 | 116 | -6 |
2 | 删除生产环境中的 eslint-loader | 116 | 95 | -21 |
3 | 删除 url-loader 对字体文件、音视频文件的校验 | 95 | 94 | -1 |
4 | babel-loader 不处理第三方依赖 | 94 | 84 | -10 |
5 | babel-loader 开启本地缓存 | 84 | 80 | -4 |
6 | 分包依赖优化、IM SDK 优化 | 80 | 74 | -6 |
合计 | 打包时间减少48s 速度提升约40% |
优化点 | 优化前体积(kb) | 优化后体积(kb) | 效果 | ||||
---|---|---|---|---|---|---|---|
主包 | 整包 | 主包 | 整包 | 主包 | 整包 | ||
1 | 分包依赖优化、IM SDK优化 | 1940 | 7144 | 1809 | 6523 | -131 | -621 |
2 | 静态资源优化 | 1809 | 6523 | 1607 | 6175 | -202 | -348 |
合计 | 主包减小17%(333kb)、整包减小13.6%(969kb) |
本文中提及的几点优化要点,对其他小程序项目有一定参考意义。mpvue 小程序框架默认的将所有公共依赖打包至主包,造成主包过大,甚至经常导致主包大小超过小程序的 2MB 上限,很不合理,我们提出的分包依赖优化可以有效解决此问题。部分编译,是一种最直接有效提升编译效率的方法,这个方法适用于任何其他项目。而 webpaclk loader 优化、静态资源优化则是比较常见的前端项目优化手段。
以上内容仅代表个人理解,不足之处欢迎讨论指正。