前端-团队效率-webpack4打包骚操作

4,293 阅读6分钟

需求背景

  • 使用angular-cli打包时当工程较大,引入过多时会出现打包卡顿卡死的情况
  • 使用webpack4自定义工程打包,面临着打包时间的问题

常见解决方案

与脚手架结合

  • 在vue-cli中vue.config.js中扩展webpack配置,依然可以实现打包效率提升
  • 在angular-cli中比较麻烦一点,limeii.github.io/2019/08/ang…,亲测由于内置的功能已经比较强大就算加上happyback也不能提升多少打包速度,反而会增加打包体积。

仔细观察发现

angular中的解决方案

  • 简单粗暴不进行作用域提升直接--optimization=false;显然速度会得到指数级提升,同样的代码体积也是暴涨,也有一定的应用场景就是写一个git提交检验时可以使用这一脚本进行配置。www.cnblogs.com/lifefriend/…,在提交之前执行这个脚本保证运维打包不会报错。
  • 其他工程中只要webpack.config.js配置 以下代码效果也是一样速度变快了,代码变大了

    optimization.concatenateModules = false

  • 提高运存 "ng-high-memory": "node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng",这是到哪都能配置的

找问题

  • 看代码卡在哪了?github.com/webpack/web…
  • 我发现里面有一个递归调用方法即_tryToAdd在测试工程项目中调用了10000多次
  • 解决思路1递归算法优化,利用闭包缓存其实只要缓存这个递归调用的结果就可以提升打包效率因为很多引用都是重复的。代码如下:
  • /*  MIT License http://www.opensource.org/licenses/mit-license.php  Author Tobias Koppers @sokra
      ModuleConcatenationPlugin.js文件
    */"use strict";const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");const ModuleHotAcceptDependency = require("../dependencies/ModuleHotAcceptDependency");const ModuleHotDeclineDependency = require("../dependencies/ModuleHotDeclineDependency");const ConcatenatedModule = require("./ConcatenatedModule");const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibilityDependency");const StackedSetMap = require("../util/StackedSetMap");const failureCache = require("./failureCache.js");// const math = require("./count.js");const formatBailoutReason = msg => {  return "ModuleConcatenation bailout: " + msg;};class ModuleConcatenationPlugin {  constructor(options) {    if (typeof options !== "object") options = {};    this.options = options;  }  apply(compiler) {    compiler.hooks.compilation.tap(      "ModuleConcatenationPlugin",      (compilation, { normalModuleFactory }) => {        const handler = (parser, parserOptions) => {          parser.hooks.call.for("eval").tap("ModuleConcatenationPlugin", () => {            // Because of variable renaming we can't use modules with eval.            parser.state.module.buildMeta.moduleConcatenationBailout = "eval()";          });        };        normalModuleFactory.hooks.parser          .for("javascript/auto")          .tap("ModuleConcatenationPlugin", handler);        normalModuleFactory.hooks.parser          .for("javascript/dynamic")          .tap("ModuleConcatenationPlugin", handler);        normalModuleFactory.hooks.parser          .for("javascript/esm")          .tap("ModuleConcatenationPlugin", handler);        const bailoutReasonMap = new Map();        const setBailoutReason = (module, reason) => {          bailoutReasonMap.set(module, reason);          module.optimizationBailout.push(            typeof reason === "function"              ? rs => formatBailoutReason(reason(rs))              : formatBailoutReason(reason)          );        };        const getBailoutReason = (module, requestShortener) => {          const reason = bailoutReasonMap.get(module);          if (typeof reason === "function") return reason(requestShortener);          return reason;        };        compilation.hooks.optimizeChunkModules.tap(          "ModuleConcatenationPlugin",          (allChunks, modules) => {            // math.count()            const relevantModules = [];            const possibleInners = new Set();            for (const module of modules) {              // Only harmony modules are valid for optimization              if (                !module.buildMeta ||                module.buildMeta.exportsType !== "namespace" ||                !module.dependencies.some(                  d => d instanceof HarmonyCompatibilityDependency                )              ) {                setBailoutReason(module, "Module is not an ECMAScript module");                continue;              }              // Some expressions are not compatible with module concatenation              // because they may produce unexpected results. The plugin bails out              // if some were detected upfront.              if (                module.buildMeta &&                module.buildMeta.moduleConcatenationBailout              ) {                setBailoutReason(                  module,                  `Module uses ${module.buildMeta.moduleConcatenationBailout}`                );                continue;              }              // Exports must be known (and not dynamic)              if (!Array.isArray(module.buildMeta.providedExports)) {                setBailoutReason(module, "Module exports are unknown");                continue;              }              // Using dependency variables is not possible as this wraps the code in a function              if (module.variables.length > 0) {                setBailoutReason(                  module,                  `Module uses injected variables (${module.variables                    .map(v => v.name)                    .join(", ")})`                );                continue;              }              // Hot Module Replacement need it's own module to work correctly              if (                module.dependencies.some(                  dep =>                    dep instanceof ModuleHotAcceptDependency ||                    dep instanceof ModuleHotDeclineDependency                )              ) {                setBailoutReason(module, "Module uses Hot Module Replacement");                continue;              }              relevantModules.push(module);              // Module must not be the entry points              if (module.isEntryModule()) {                setBailoutReason(module, "Module is an entry point");                continue;              }              // Module must be in any chunk (we don't want to do useless work)              if (module.getNumberOfChunks() === 0) {                setBailoutReason(module, "Module is not in any chunk");                continue;              }              // Module must only be used by Harmony Imports              const nonHarmonyReasons = module.reasons.filter(                reason =>                  !reason.dependency ||                  !(reason.dependency instanceof HarmonyImportDependency)              );              if (nonHarmonyReasons.length > 0) {                const importingModules = new Set(                  nonHarmonyReasons.map(r => r.module).filter(Boolean)                );                const importingExplanations = new Set(                  nonHarmonyReasons.map(r => r.explanation).filter(Boolean)                );                const importingModuleTypes = new Map(                  Array.from(importingModules).map(                    m => /** @type {[string, Set]} */ ([                      m,                      new Set(                        nonHarmonyReasons                          .filter(r => r.module === m)                          .map(r => r.dependency.type)                          .sort()                      )                    ])                  )                );                setBailoutReason(module, requestShortener => {                  const names = Array.from(importingModules)                    .map(                      m =>                        `${m.readableIdentifier(                          requestShortener                        )} (referenced with ${Array.from(                          importingModuleTypes.get(m)                        ).join(", ")})`                    )                    .sort();                  const explanations = Array.from(importingExplanations).sort();                  if (names.length > 0 && explanations.length === 0) {                    return `Module is referenced from these modules with unsupported syntax: ${names.join(                      ", "                    )}`;                  } else if (names.length === 0 && explanations.length > 0) {                    return `Module is referenced by: ${explanations.join(                      ", "                    )}`;                  } else if (names.length > 0 && explanations.length > 0) {                    return `Module is referenced from these modules with unsupported syntax: ${names.join(                      ", "                    )} and by: ${explanations.join(", ")}`;                  } else {                    return "Module is referenced in a unsupported way";                  }                });                continue;              }              possibleInners.add(module);            }            // sort by depth            // modules with lower depth are more likely suited as roots            // this improves performance, because modules already selected as inner are skipped            relevantModules.sort((a, b) => {              return a.depth - b.depth;            });            const concatConfigurations = [];            const usedAsInner = new Set();            for (const currentRoot of relevantModules) {              // when used by another configuration as inner:              // the other configuration is better and we can skip this one              if (usedAsInner.has(currentRoot)) continue;              // create a configuration with the root              const currentConfiguration = new ConcatConfiguration(currentRoot);              // cache failures to add modules              // const failureCache = new Map();                            // const failureCache = new Map();              // try to add all imports              for (const imp of this._getImports(compilation, currentRoot)) {                let hasProblem = failureCache.get(imp);                if (hasProblem) {                  // math.count()                  currentConfiguration.addWarning(imp, hasProblem);                }                else{                  const problem = this._tryToAdd(                    compilation,                    currentConfiguration,                    imp,                    possibleInners,                    failureCache                  );                  if (problem) {                    failureCache.set(imp, problem);                    currentConfiguration.addWarning(imp, problem);                  }                }                            }              if (!currentConfiguration.isEmpty()) {                concatConfigurations.push(currentConfiguration);                for (const module of currentConfiguration.getModules()) {                  if (module !== currentConfiguration.rootModule) {                    usedAsInner.add(module);                  }                }              }            }            // HACK: Sort configurations by length and start with the longest one            // to get the biggers groups possible. Used modules are marked with usedModules            // TODO: Allow to reuse existing configuration while trying to add dependencies.            // This would improve performance. O(n^2) -> O(n)            concatConfigurations.sort((a, b) => {              return b.modules.size - a.modules.size;            });            const usedModules = new Set();            for (const concatConfiguration of concatConfigurations) {              if (usedModules.has(concatConfiguration.rootModule)) continue;              const modules = concatConfiguration.getModules();              const rootModule = concatConfiguration.rootModule;              const newModule = new ConcatenatedModule(                rootModule,                Array.from(modules),                ConcatenatedModule.createConcatenationList(                  rootModule,                  modules,                  compilation                )              );              for (const warning of concatConfiguration.getWarningsSorted()) {                newModule.optimizationBailout.push(requestShortener => {                  const reason = getBailoutReason(warning[0], requestShortener);                  const reasonWithPrefix = reason ? ` (<- ${reason})` : "";                  if (warning[0] === warning[1]) {                    return formatBailoutReason(                      `Cannot concat with ${warning[0].readableIdentifier(                        requestShortener                      )}${reasonWithPrefix}`                    );                  } else {                    return formatBailoutReason(                      `Cannot concat with ${warning[0].readableIdentifier(                        requestShortener                      )} because of ${warning[1].readableIdentifier(                        requestShortener                      )}${reasonWithPrefix}`                    );                  }                });              }              const chunks = concatConfiguration.rootModule.getChunks();              for (const m of modules) {                usedModules.add(m);                for (const chunk of chunks) {                  chunk.removeModule(m);                }              }              for (const chunk of chunks) {                chunk.addModule(newModule);                newModule.addChunk(chunk);              }              for (const chunk of allChunks) {                if (chunk.entryModule === concatConfiguration.rootModule) {                  chunk.entryModule = newModule;                }              }              compilation.modules.push(newModule);              for (const reason of newModule.reasons) {                if (reason.dependency.module === concatConfiguration.rootModule)                  reason.dependency.module = newModule;                if (                  reason.dependency.redirectedModule ===                  concatConfiguration.rootModule                )                  reason.dependency.redirectedModule = newModule;              }              // TODO: remove when LTS node version contains fixed v8 version              // @see https://github.com/webpack/webpack/pull/6613              // Turbofan does not correctly inline for-of loops with polymorphic input arrays.              // Work around issue by using a standard for loop and assigning dep.module.reasons              for (let i = 0; i < newModule.dependencies.length; i++) {                let dep = newModule.dependencies[i];                if (dep.module) {                  let reasons = dep.module.reasons;                  for (let j = 0; j < reasons.length; j++) {                    let reason = reasons[j];                    if (reason.dependency === dep) {                      reason.module = newModule;                    }                  }                }              }            }            compilation.modules = compilation.modules.filter(              m => !usedModules.has(m)            );          }        );      }    );  }  _getImports(compilation, module) {    return new Set(      module.dependencies        // Get reference info only for harmony Dependencies        .map(dep => {          if (!(dep instanceof HarmonyImportDependency)) return null;          if (!compilation) return dep.getReference();          return compilation.getDependencyReference(module, dep);        })        // Reference is valid and has a module        // Dependencies are simple enough to concat them        .filter(          ref =>            ref &&            ref.module &&            (Array.isArray(ref.importedNames) ||              Array.isArray(ref.module.buildMeta.providedExports))        )        // Take the imported module        .map(ref => ref.module)    );  }  _tryToAdd(compilation, config, module, possibleModules, failureCache) {    // math.countFor();    const cacheEntry = failureCache.get(module);    if (cacheEntry) {      // math.count()      return cacheEntry;    }    // Already added?    if (config.has(module)) {      return null;    }    // Not possible to add?    if (!possibleModules.has(module)) {      failureCache.set(module, module); // cache failures for performance      return module;    }    // module must be in the same chunks    if (!config.rootModule.hasEqualsChunks(module)) {      failureCache.set(module, module); // cache failures for performance      return module;    }    // Clone config to make experimental changes    const testConfig = config.clone();    // Add the module    testConfig.add(module);    // Every module which depends on the added module must be in the configuration too.    for (const reason of module.reasons) {      // Modules that are not used can be ignored      if (        reason.module.factoryMeta.sideEffectFree &&        reason.module.used === false      )        continue;      let hasProblem = failureCache.get(reason.module);      if (hasProblem) {        // math.count()        return hasProblem;      }else{        const problem = this._tryToAdd(          compilation,          testConfig,          reason.module,          possibleModules,          failureCache        );        if (problem) {          failureCache.set(module, problem); // cache failures for performance          return problem;        }      }    }    // Commit experimental changes    config.set(testConfig);      // Eagerly try to add imports too if possible    for (const imp of this._getImports(compilation, module)) {      let hasProblem = failureCache.get(module);      if (hasProblem) {        // math.count()        config.addWarning(imp, hasProblem);      }      else{        const problem = this._tryToAdd(          compilation,          config,          imp,          possibleModules,          failureCache        );        if (problem) {          failureCache.set(module, problem);          config.addWarning(imp, problem);        }      }    }    return null;  }}class ConcatConfiguration {  constructor(rootModule, cloneFrom) {    this.rootModule = rootModule;    if (cloneFrom) {      this.modules = cloneFrom.modules.createChild(5);      this.warnings = cloneFrom.warnings.createChild(5);    } else {      this.modules = new StackedSetMap();      this.modules.add(rootModule);      this.warnings = new StackedSetMap();    }  }  add(module) {    this.modules.add(module);  }  has(module) {    return this.modules.has(module);  }  isEmpty() {    return this.modules.size === 1;  }  addWarning(module, problem) {    this.warnings.set(module, problem);  }  getWarningsSorted() {    return new Map(      this.warnings.asPairArray().sort((a, b) => {        const ai = a[0].identifier();        const bi = b[0].identifier();        if (ai < bi) return -1;        if (ai > bi) return 1;        return 0;      })    );  }  getModules() {    return this.modules.asSet();  }  clone() {    return new ConcatConfiguration(this.rootModule, this);  }  set(config) {    this.rootModule = config.rootModule;    this.modules = config.modules;    this.warnings = config.warnings;  }}module.exports = ModuleConcatenationPlugin;

  • 代码中 failureCache对象进行缓存处理即可提高打包效率
  • 方案二多核打包juejin.cn/post/684490…,还未实现

骚操作

  • 我是把webpack改了打包效率提升了,怎么应用到我们自己工程当中呢?
  • 第一步把自己项目node-module的webpack拷贝出来,并修改代码
  • 第二步建立私有仓库juejin.cn/post/684490…
  • 第三步把自己修改的webpack上传到私有仓库
  • 第四步卸载本工程的webpack,uninstall
  • 第五步install 私有仓库的修改后的webpack,名字都不用换

举一反三

  • 不仅仅是wepack中的任何一个插件,乃至于我们在项目用引用其他组件,都有在git上面clone下来进行私有化改造,再上传到私有仓库中。
  • 思考问题从根源处解决永远是一劳永逸的,不管怎么改webpack的配置源码级别的性能问题是永远解决不了的。
  • 完结撒花。。。。。。。。。。。。。。。。。。。。。。。。。。。。。