前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端-团队效率-webpack4打包骚操作

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

作者头像
吴文周
发布2020-01-17 16:16:06
8260
发布2020-01-17 16:16:06
举报
文章被收录于专栏:吴文周的专栏吴文周的专栏

需求背景

  • 使用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(warning0, requestShortener); const reasonWithPrefix = reason ? (<- ${reason}) : ""; if (warning0 === warning1) { 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.dependenciesi; if (dep.module) { let reasons = dep.module.reasons; for (let j = 0; j < reasons.length; j++) { let reason = reasonsj; 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 = a0.identifier(); const bi = b0.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.im/post/5d8cb1…,还未实现

骚操作

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

举一反三

  • 不仅仅是wepack中的任何一个插件,乃至于我们在项目用引用其他组件,都有在git上面clone下来进行私有化改造,再上传到私有仓库中。
  • 思考问题从根源处解决永远是一劳永逸的,不管怎么改webpack的配置源码级别的性能问题是永远解决不了的。
  • 完结撒花。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年12月10日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 需求背景
  • 常见解决方案
  • 与脚手架结合
  • 仔细观察发现
  • angular中的解决方案
  • 找问题
  • 骚操作
  • 举一反三
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档