本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
前言
最近写 H5 的项目比较多,该项目从年龄上看着还算比较年轻😂,整个架构应该是直接使用 vue-cli 基于 vue2 生成的,那底层打包工具自然也就是 webpack,我们知道 webpack 有个通病,那就是随着项目的不断增大每次构建的时间也会随之越来越长。比如我们这个项目的单次冷启动就达到了惊人的 1 分 20 秒左右,每次跑完电脑风扇转的飞起,简直忍不了!(可能是电脑太老了)
下面一起看看如何将项目的冷启动时长从 1 分 20 秒左右优化到十几秒左右吧~
「如果这篇文章有帮助到你,❤️关注 + 点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~」
是什么让构建效率这么慢?
页面数量
由于我们这个项目是个 SPA 项目,路由是通过vue-auto-routing来自动生成的。为了更直观的看到里面有多少个页面,于是我把routes打印出来了。
居然有 258 个之多!页面这么多,webpack 打包构建的速度自然就会慢。
时间都用在哪?
为了对项目做一些有针对性的优化,我们需要了解整个编译过程中耗时分布,知道了各模块的耗时数据我们才能对症下药。
这里可以使用speed-measure-webpack-plugin插件来进行分析。
❝
speed-measure-webpack-plugin不仅可以分析总的打包时间,还能分析各阶段 loader 的耗时。❞
// 使用const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')const plugins = [ // ... new SpeedMeasurePlugin(),]从上图来看,编译过程中的大部分时间都是用在 vue 文件的编译处理 loader 上。「说白了还是文件太多了导致编译耗时比较长。」
如何优化?
通用方案
开启缓存
webpack 中几种缓存方式:
「
cache-loader」「
hard-source-webpack-plugin」「
babel-loader 的 cacheDirectory 标志」
我们这个项目使用的vue-cli版本是4.1.0,它已经内置了 「cache-loader」 和 babel-loader 的 cacheDirectory 标志两种缓存
我们可以二次启动看看
二次启动花费了大概 43 秒,提升还是蛮大的,主要原因是 "冷启动" 时已经将 「babel-loader、vue-loader」 进行了缓存:
另外一种缓存可自行测试 hard-source-webpack-plugin「,主要缓存这种方案只在二次启动才能有明显的性能提升,与我首次冷启动就要」**快 ** 的预期不符。这种方案这里就不再试了
开启多线程
由于 js 单线程的特点,当有多个任务同时存在,它们也只能排队串行执行。
所以有没有可以使用类似web Worker的技术实现多线程编译处理,将部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间。
「可选方案:」
「thread-loader」(官方推出)
「parallel-webpack」
「HappyPack」
从上面可以发现,编译过程,大部分时间都是在处理 vue 文件,所以可以针对vue-loader使用thread-loader
{ test: /\.vue$/, include: path.resolve('src'), use: [ { loader: 'thread-loader', options: { workers: 2, }, }, ],},❝
「注意」:仅在耗时的操作中使用 「thread-loader」,否则使用 「thread-loader」 会后可能会导致项目构建时间变得更长,因为每个 「worker」 都是一个独立的 「node」 进程,创建 worker 的过程也是耗时的,尽量不要得不偿失。
❞
此时的编译时间为 41 秒左右,提升好像并不是特别明显,可能在大型项目中才会发挥出更大的作用。
当然还有很多方案可以一一尝试,但我觉得达到的效果应该都不会超过我接下来的针对性方案。
针对性方案
❝
该方案其实就是缩小我们的构建目标,整个项目虽然有很多页面,从上面路由来看多达 258 个,但我们平时在开发过程中其实只关注我们当前需要修改的页面,所有有没有可能在开发过程中,我只构建我需要用的页面,对于那些不需要的页面不参与构建,这样的话肯定能够大幅提升我们的本地构建时间。
❞
「这里还需要考虑的是,怎么对原有构建代码的侵入性做到最小?」
思路
新增构建脚本,原有
npm run dev保持不变处理需要启动的页面,生成对应的路由
routes.dev.js把原有
routes提取成文件routes.pro.js再通过
NormalModuleReplacementPlugin插件在编译过程进行文件替换最后再进行构建
构建脚本
新增 start 命令
// package.json"start": "node ./build/cli.js start",主要构建代码如下
// cli.jsconst shell = require('shelljs')const path = require('path')const fs = require('fs')const action = process.argv[2]const arg = process.argv.slice(3)let appName = arg[0] // 指的是你要启动的项目(文件夹名)// const startPath = arg.join('/')console.log('🚀🚀------start------🚀🚀');(() => { if (!appName) { // 未输入项目名称则开启交互命令行 openInquirer() return } // 启动 if (action === 'start') { start() }})()function start() { // console.log('启动项目') process.env.action = 'signle' runTask(appName)}// 启动项目async function runTask(appName) { const cmds = [] console.log(`🚢【启动项目】${appName}`) generateRoute(appName) // 生成需要启动的路由 const runProPath = path.resolve(__dirname, `../src/pages/${appName}`) // if (process.platform === 'win32') { // cmds.push(`set runProPath=${runProPath}`) // } else { // cmds.push(`export runProPath=${runProPath}`) // } // 检测项目是否存在 const res = await getProject(runProPath) if (res.errno < 0) { // 抛出异常 throw new Error('没有找到可启动的项目😭') } else { cmds.push(`npx vue-cli-service serve --open --colors --mode dev`) } const cmd = cmds.join(' && ') // return const { code } = shell.exec(cmd) return code}处理需要启动的页面
由于这个项目是用vue-auto-routing来自动生成路由的,所以这里我依然还是用它内部的一个库来自动生成
const { generateRoutes } = require('vue-route-generator')// 处理需要启动的路由function generateRoute() { console.log('--', path.resolve(__dirname, `../src/pages/${appName}/`)) const code = generateRoutes({ pages: path.resolve(__dirname, `../src/pages/${appName}/`), importPrefix: `@/pages/${appName}/`, }) fs.writeFileSync(path.resolve(__dirname, `../src/routes.dev.js`), code)}替换需要启动的路由
根据用户输入的需要启动的文件夹名,我们为这个文件夹内的所有文件自动成了路由文件routes.dev.js,现在需要做的是通过 webpack 进行替换。
new webpack.NormalModuleReplacementPlugin( /src\/routes.pro.js/, './routes.dev.js', ),使用
主要的工作完成,现在可以来启动试一试
比如:启动某 ** 项目
# 启动命令 npm start + 项目名称(文件夹名)npm start campusArea现在的启动时间大概在 15 秒左右,这与你当前文件夹下的文件数量有关,文件越少启动越快!二次启动时间大概在 10 秒左右,小项目首次启动时长大概都在 10 秒内
「首次冷启动时长大概节省了 1min,写代码的时间又变多了😂」
优化
可能大家都习惯了npm run dev或npm start,会忘记启动页面的参数?
不要急,这一点也考虑进去了,有个非常强大的库inquirer可以为我们开启交互式命令行。
// 未输入项目名称则开启交互命令行function openInquirer() { // 获取所有可启动目录 const projectList = fs.readdirSync(path.resolve(__dirname, '../src/pages')) // console.log('projectList', projectList) const promptList = [ { type: 'list', message: '🚗请选择启动的目录:', name: 'pro', choices: [...projectList], }, ] inquirer.prompt(promptList).then((answers) => { console.log(answers) appName = answers.pro start() })}当你直接npm start的时候,可以让你选择你想要启动的目录:
结束🚀🚀🚀。
「如果这篇文章有帮助到你,❤️关注 + 点赞❤️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~」