webpack多页面内存溢出&单页面编译慢

8,212 阅读7分钟

因为自己的项目是基于vue-cli3进行开发,所以这里只讨论这种情况下的解决办法 在进行多页面开发的时候,项目刚开始阶段,页面较少,编译速度还能忍受,但是一旦页面增加,多次热更新就造成了内存溢出。

原因

这里需要借助一个插件来进行性能分析webpack-bundle-analyzer,在vue.config.js中添加以下代码

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
configureWebpack: {
 plugins: [
    new BundleAnalyzerPlugin(),
 ],
}

下面是自己项目编译的截图

可以看到的是webpack把所有的页面都进行了编译,总体积已经达到了18M,耗时超过1分钟,在热更新的时候这个体积会变得更大,从而占据node的运行内存,导致内存溢出。但是一般在开发的时候,我们一次更改的页面可能就只有几个,所以编译这些多余的页面是没有必要的。那下面就是多种解决方案

下面就是几种尝试的方法,加快编译的速度

增加Node运行内存

在上面提到在热更新的时候,热更新的代码会大量占据node分配的内存,导致内存溢出。那么第一种方式,尝试增加node的运行内存。在Node中通过JavaScript使用内存时只能使用部分内存(64位系统下约为1.4 GB32位系统下约为0.7 GB)。所以不管电脑实际的运行内存是多少,Node在运行代码编译的时候,使用内存大小不会发生变化。这样就可能导致因为原有的内存不够,导致内存溢出。下面给出两种方案

更改cmd

node_modules/.bin/vue-cli-server.cmd把下面代码复制上去

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe" --max_old_space_size=4096 "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node --max_old_space_size=4096  "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
)

更改package.json

把启动Node服务的更改下:

node --max_old_space_size=4096 node_modules/@vue/cli-service/bin/vue-cli-service.js serve

本质上没什么区别,都是增加Node分配的内存,在这里把node的运行内存提高到4g就能够让webpack热编译的时候不会内存溢出。使用这种方法,确实是没有在出现内存溢出的情况。但是在首次启动和热编译的时候,速度并没有发生质的提升,首次编译还是达到了1分钟这种难以忍受的速度。如果项目进一步扩大,难道我们需要再次增加node的运行内存?

过滤编译页面

在上面截图中可以看到,在编译的时候webpack编译了一些不必要的页面,本来我们只需要调试A页面,但是webpack把所有的页面都进行了编译,某些页面我们可能并不需要,那这里就提出一种思路,对需要调试的页面进行过滤。 下面是多页面的配置:

// page.config.js
module.exports = {
    index: {
        entry: 'src/page/index/main.js', // 页面入口
        template: 'public/index.html', // 页面模板路径
        filename: 'index.html' // 输出文件名
        title: '页面title',
    }
}
// vue.config.js
const pages = require('./page.config.js')
module.expors = {
    pages,
}

可以看到的是传统的方案是把多页面的配置全部引入,进行编译,所以这里就提出一种解决方案,对多页面进行过滤,得到我们需要的编译页面,下面是过滤的脚本:

const path = require('path');
const fs = require('fs');
const pages = require('../pages.config');

const params = JSON.parse(process.env.npm_config_argv).original;
const buildPath = params[params.length - 1].match(/[a-zA-Z0-9]+/)[0] || '';

let buildConfig = {
  pages: [],
};


if (!/(test|online|serve)/gi.test(buildPath)) {
  const configJsPath = path.resolve(__dirname, `${buildPath}.js`);

  // 如果该路径存在
  if (fs.existsSync(configJsPath)) {
    // eslint-disable-next-line import/no-dynamic-require
    buildConfig = require(configJsPath);
  } else if (pages[buildPath]) {
    buildConfig.pages = buildPath.split(',');
  } else {
    throw new Error('该路径不存在');
  }
} else {
  buildConfig = require('./default');
}
const buildPages = {};
buildConfig.pages.forEach((name) => {
  buildPages[name] = pages[name];
});
module.exports = buildPages;

这样就可以单独单独编译我们所需要的页面下面是default.js的内容:

module.exports = {
    pages: ['ugcDetail']
}

这个文件中pages就是我们需要编译的文章,现在webpack就过滤了不需要编译的页面,下面是页面编译速度的截图:

页面的编译速度提高了惊人的10倍,因为需要编译的文件少了,所以运行速度也就提高了不少。一般情况下,一个人是负责单独的业务线,别人的代码我们也不需要干涉,所以也能实现一次配置,多次运行的效果。但是有的时候我们也需要更改别人的代码,不能说又加一个配置文件,下面就是借助webpack自带的钩子实现编译指定文件。

使用webbpack-dev-serve钩子进行单独编译

在上面page.config.js中可以看到每个单页面都有一个入口文件,webpack借助这些入口文件进行对每个页面进行单独编译,每个页面编译后的js混合到一起也就非常大了,那我们能不能让这些入口文件暂时变成一个空文件,如果需要编译这个页面,在空文件中引入需要编译的入口文件。也就是所有的入口文件都变成了一个空的js文件,如果需要编译这个页面,在通过

import 入口

实现单独的页面文件编译。那webpack是如何知道我们需要编译的页面呢,在webpack-dev-serve中,有一个钩子before,在访问页面的时候我们能够拿到页面信息的路径,下面是实现:

// vue.config.js
const compiledPages = [];
before(app) {
      app.get('*.html', (req, res, next) => {
        const result = req.url.match(/[^/]+?(?=\.)/);
        const pageName = result && result[0];
        const pagesName = Object.keys(multiPageConfig);

        if (pageName) {
          if (pagesName.includes(pageName)) {
            if (!compiledPages.includes(pageName)) {
              const page = multiPageConfig[pageName];
              fs.writeFileSync(`dev-entries/${pageName}.js`, `import '../${page.tempEntry}'; // eslint-disable-line`);
              compiledPages.push(pageName);
            }
          } else {
            // 没这个入口
            res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
            res.end('<p style="font-size: 50px;">不存在的入口</p>');
          }
        }
        next();
      });
    },

多页面配置中以下配置,需要先把入口路径,先缓存起来,然后置空,下面是具体实现

{
    pageName: {
        entry: entryPath,
        chunks: [array]
    }
}
const fs = require('fs');
const util = require('util');

const outputFile = util.promisify(fs.writeFile);
async function main() {
  const tasks = [];
  if (!fs.existsSync('dev-entries')) {
    fs.mkdirSync('dev-entries');
  }
  Object.keys(pages).forEach((key) => {
    const entry = `dev-entries/${key}.js`;
    pages[key].tempEntry = pages[key].entry; // 暂存真正的入口文件地址
    pages[key].entry = entry;
    tasks.push(outputFile(entry, ''));
  });
  await Promise.all(tasks);
}

if (process.env.NODE_ENV === 'development') {
  main();
}

module.exports = pages;

这种方法就是把所有页面入口文件置为空文件,虽然编译了所有的页面但是所有的文件都是空的,所以大大的减少了首次编译的文件大小。

速度也从原来的80多秒,降低到了8s。然后当我们访问某个页面的页面,执行到before钩子,进行单独编译,速度也是非常快的。

优化脚本

针对上面代码进行优化,上面实际上虽然把所以文件入口都制成了空,但是实际上还有这个空文件进行打包了,所以现在优化点在于只要这个页面没有调试,我们就不对他进行打包,这种方式还有个好处就是一般情况下都有测试环境,对于个人而言,很多时候需要把这个代码部署到测试机上。上面的方法解决了开发环境编译慢的情况,但是如果要把代码上传到测试机上,就一点帮助没有,需要上传所有的页面文件。所以给出下面优化的方式,这次我们不更改page.config.js的代码。
script文件夹下面创建一个json文件,里边存放一个最简单的页面入口,这个页面打包出来最好什么都没有。例如:

{
  "miguRead": {
    "entry": "src/pages/miguRead/main.js",
    "template": "public/index.html",
    "filename": "miguRead.html",
    "title": "咪咕作业帮悦读会",
    "business": "base"
  }
}

然后在修改vue.config.js

const developemntPages = require('./help')
let compiledPages;
if (process.env.NODE_ENV === 'development') {
  compiledPages = developmentPages;
  compiledPages.miguRead = multiPageConfig.miguRead;
} else {
  compiledPages = multiPageConfig;
}
before: (app) {
    //...
    if (pageName) {
        if (multiPageConfig[pageName]) {
            if (!compiledPages[pageName]) {
              compiledPages[pageName] = multiPageConfig[pageName];
              fs.writeFileSync('help.json', `
                ${JSON.stringify(compiledPages)}
              `);
            }
        } else {
            res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
            res.end('<p style="font-size: 50px;">不存在的入口</p>');
        }
    }

}

因为每次重启服务的时候,这些变量也会重置,所以需要借助一个不会重启而修改的文件,所以这里引入了一个help.json。最后在package.json中加入监听help.json这个程序,当我们访问某个页面的时候,就会将该页面的信息写入help.json。重启服务后,就能拿到该页面的信息。package.json中更改的信息为

nodemon --watch vue.config.js --watch help.json --exec node --max_old_space_size=4096 node_modules/@vue/cli-service/bin/vue-cli-service.js serve

升级html-webpack-plugin版本

多页面出现内存溢出的问题是因为在编译的时候,实际是一次更改,编译了多个文件,这是html-webpack-plugin的问题。因为没生成一个页面,就需要调用一下new htmlWebpackPlugin(),多个页面的时候内存就不够用了。所以改一下这个这个webpack插件的版本,升级到4.0.0-beta.8这个版本。然后再vue.config.js中添加下面的配置,这样也不会造成内存溢出。

const htmlPlugins = [];
Object.keys(multiPageConfig).forEach((key) => {
    htmlPlugins.push(multiPageConfig[key])
})
configureWebpack: {
    plugins: [
      ...htmlPlugins,
    ],
}

单页面热编译过慢

后台技术栈使用vue-cli3,由于后台项目汇聚了一个部门的后台服务,采用了单页面多路由的方案,所以导致路由特别多。刚开始路由较少的时候,编译起来速度还是挺快的,当页面增加到100个的时候,那速度简直没法忍,比前台的多页面还慢,启动的时候需要等几分钟。所以在网上找了解决方法,需要在开发环境的时候将所有的异步组件都用同步的方式引入,具体为啥这么做,我也不知道,有兴趣的小伙伴可以研究一下。

解决方案

安装babel-plugin-dynamic-import-node。它制作一件事就是将所有的import()转换为require(),这样就可以用这个插件将所有的异步组件都用同步的方式引入,并结合BABEL_ENV这个babel环境变量,让它只作用于开发环境下。具体做法如下,在babel.config.js加入

env: {
    development: {
      plugins: ['dynamic-import-node'],
    },
  },

vue-cli3有对应的env文件,例如我的就是.env.development,然后添加环境变量

VUE_CLI_BABEL_TRANSPILE_MODULES=true

OK,大功告成,编译速度快了15倍不止,赶快去试试吧。

这几种解决多页面内存溢出的方法各有优缺点,读者可根据自己的项目自行决定使用哪种方法,可能有时还需要多种方式组合使用,就看看那个好使好用了。
推销一波自己的github最近在抓紧学习,会持续更新文章,希望大家多多关注。

Log:

11-13: 优化多页面打包方案,解决多路由编译慢