炫酷表白网站在线制作,网站服务器租用 价格,免费素材网站可商用,网站商城建设基本流程大家好#xff0c;我是若川。我持续组织了近一年的源码共读活动#xff0c;感兴趣的可以 点此扫码加我微信 lxchuan12 参与#xff0c;每周大家一起学习200行左右的源码#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试…大家好我是若川。我持续组织了近一年的源码共读活动感兴趣的可以 点此扫码加我微信 lxchuan12 参与每周大家一起学习200行左右的源码共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外目前建有江西|湖南|湖北籍前端群可加我微信进群。扫码加我微信 lxchuan12、拉你进源码共读群本文作者文西前言代码体积的控制对前端来说至关重要尽管网络条件逐渐变好但是代码体积的增加不仅仅只影响资源加载速度还会直接或间接影响浏览器各类性能指标。例如增加用户内存使用消耗内存的增加又会更频繁的触发 V8 引擎的 GC 机制进而影响页面交互性能。本文从一个典型的 WebpackBabel 工程出发找到构建产物体积变大的常见原因和对应的解决思路减少项目代码构建后的体积Babelbabel 最常见的用途就是代码降级使构建后的代码能够被低版本浏览器兼容按照功能可以划分两部分API 降级语法降级通过 Babel 构建后的代码为了适配低版本浏览器通常会比源代码大上几倍这里面除了源代码外还包含 API 垫片和语法辅助函数分别对应上诉的 API 降级和语法降级我们看下如何减少这部分的代码体积core-js 按照目前最新版本的 babel7babel/polyfill 已经废弃我们使用 core-js 完成 API 的语法降级core-js 可以为浏览器中可能不兼容的 API 提供垫片例如 PromiseMapimport core-js/modules/es.promise.js;// 使用降级 API
const promise Promise.resolve();在需要降级的 API 调用前 require 对应的 core-js 模块就可以以污染全局变量或者原型链的方式实现 API 降级手动插入 core-js 即麻烦又不安全所以我们可以使用babel/preset-env帮助我们自动插入 core-js 模块babel/preset-env根据项目中 browserlist 定义的用户环境选择性插入垫片代码减少垫片代码体积在配置babel/preset-env 时useBuiltIns 属性非常重要有两个值entry|usage分别为全量降级和按需降级entry 全量降级entry 非常直接首先我们需要手动在代码的第一行import core-js在执行编译时会按照 browserlist 中定义的环境把可能需要降级的 API 一次性插入并替换到 core-js 声明的位置开发者不再需要手动插入垫片但这有个问题即没有使用的 API 仍然会被打进 bundle 中由于 ECMAScript 标准的不断发展core-js 在 g-zip 压缩后也有 50kb 左右的体积显然还是太大了usage 按需降级当选择 usage 时babel 会扫描所有需要编译的 JS 代码根据实际使用到的 API 选择性插入所需垫片看起来是相比 entry 的更优解但实际过于理想通常基于编译速度的考虑node_modules 下的模块不会参与 Babel 编译仅参与 Webpack 打包如果此时恰巧某个依赖包里没有声明所需的垫片那么就可能出现垫片缺失最终导致线上环境 JS 运行异常。实际上这种情况在混乱的 npm 生态中非常普遍有不少 npm 包直接使用 tsc 打包除非开发者手动介入否则构建产物中就会缺少 API 垫片遇到这种情况往往只能在线上发现异常后手动添加依赖到babel.include中进行编译并不是所有 JS 代码都会参与编译例如通过一些平台动态下发的脚本这些平台动态下发的代码完全不经过编译如果使用了未经降级的 api 也可能会出现 JS 运行异常。可以看到 entryusage 都是存在问题的所以也就有了平台化的方案polyfill.io。如果使用最新的现代化浏览器访问该服务那么返回的 JS 内容则是空的反之它会响应浏览器所需的降级 API既控制了包体积也能确保未经编译的 JS 获得降级 API。Untitled出于安全考虑我们需要自部署服务目前 polyfill.io 的 node.js 代码是完全开源的支持自部署但是实际落地还需要考虑缓存和异常兜底babel/runtimecore-js 是为了解决 API 降级问题存在的但是我们还有语法降级需要解决例如 classasync默认情况下 babel 为了实现 class 功能会生成一些内联辅助函数例如下图的 createClass。这会产生一个问题就是当多个模块都使用 class 语法时则会生成多个相同的辅助函数辅助函数不能复用Untitled我们可以通过注册 babel 插件babel/plugin-transform-runtime将硬编码辅助函数的方式改为从babel/runtime引入辅助函数实现不同模块间辅助函数的复用Untitled从下图可以看到 createClass 函数从硬编码改为require(babel/runtime/helpers/createClass)代码大幅缩小Untitled但是babel/plugin-transform-runtime的方案也不是毫无问题和 api 降级一样同样面临各种依赖包构建不标准带来的困扰最大的问题就是没有办法保证依赖包的产物一定使用了babel/plugin-transform-runtime进行构建语法降级使用了内联的辅助函数又或者使用了老版本的babel-runtime·导致项目最终的构建产物对辅助函数进行了多次打包以相对常见的依赖包构建工具 father-build 和 tsc 为例他们都没有将语法辅助函数通过babel/runtime依赖包进行提取而是都以硬编码的形式存在每个 JS 模块当中。这类由社区维护的 npm 包我们不好处理但是可以通过收敛公司内部构建工具的方式统一处理公司内部维护的依赖包使它们构建的产物符合应用打包的需求我们在文章结尾处再说Tree-shakingtree-shaking 是减少构建产物体积最有效的方式以常用 lodash 为例g-zip 后的体积 24kb但是项目中使用到的函数并不多如果能够为它启用 tree-shaking代码体积能控制在 1kb 以内如何为依赖代码启用 tree-shakingpackage.json 声明 module 字段地址指向 ESM 规范的构建产物package.json 声明sideEffects:false告诉 Webpack 整个依赖包没有存在副作用或者指明存在副作用模块的地址ESMESM 相比 commonjs 具备静态分析能力 这是 tree-shaking 的前置依赖条件所以我们需要 babel 构建我们的源代码时保留 import 语法不要编译成 commonjs{presets: [[babel/preset-env,{modules: false // 保留ESM语法}]]
}sideEffects为什么依赖包的 package.json 需要声明 sideEffects这里需要引申出自函数式编程中的纯函数和副作用函数概念如果我们的代码没有存在任何副作用tree-shaking 确实可以不需要类似 sideEffects 的副作用声明但实际上副作用普遍存在我们的代码中如果只依据函数是否被引用过作为 DCE(Dead Code Elimination) 的条件很容易影响程序运行的正确性通过 css-loader 引入 css 文件是很典型的例子import ./button.css;对于 webpack 来说 button.css 同样是一个模块这里没有引用任何的具名函数但是引入 css 模块是会为我们带来一个副作用它会为 html 插入一个 style 标签。如果 webpack 认为他是没有副作用的那么在 minify 阶段 webpack 会删除这行代码最终导致样式错乱为了告诉 webpack 这个 css 文件是存在副作用的不能删除sideEffects 就可以怎么写{sideEffects: [*.css, *.less]
}公司内部维护的依赖相比开源社区很容易忽略sideEffects的声明如果存在公司内部的依赖构建工具可以将sideEffects添加到相关的模板代码中默认为依赖包开启 tree-shaking回到社区现状我们再来看 tree-shakinglodash 推出了支持 tree-shaking 的lodash-esantd4 也不再需要安装babel-plugin-import插件可以通过 tree-shaking 的方式原生支持代码按需加载从而大幅缩小构建体积Duplicate dependencies 重复依赖依赖重复打包是前端开发中的常见问题容易出现在公司内部长期无人维护的依赖包中当我们的项目中存在 Root→C→D2.0.0Root→B→D3.0.0类似的依赖关系时node_module 结构如下node_modules-- C -- depends on D2.0.0-- D2.0.0-- B -- depends on D3.0.0-- node_modules-- D3.0.0可以看到在 node_modules 下嵌套安装了 2 个版本的依赖 D即D2.0.0D3.0.0。这可能导致在构建的产物中也同样存在两份相同依赖不同版本的代码除了会影响代码体积还可能导致代码运行异常解决方式是升级 B 的依赖D2.0.0→D3.0.0此时重新安装后node_modules的嵌套结构会恢复扁平node_modules-- C -- depends on D3.0.0-- D3.0.0-- B -- depends on D3.0.0我们可以使用find-duplicate-dependencies和webpack-bundle-analyzer这些工具辅助我们排查依赖重复打包的问题最佳实践回顾文章我们对一个典型前端应用可能影响 Bundle 体积的因素进行了分析同时提出对应的解决方案。在文章的结尾我们可以更进一步通过工程化和平台化的手段以相对一劳永逸的方式解决上诉问题如下图company/app-builder负责构建应用company/module-builder负责构建依赖包然后通过使用封装的 babel 配置company/babel-base统一处理 JS 编译Untitledbabel-base关闭 core-js 的 api 降级由 app-builder 开启平台 polyfill.io 方案同时babel-base开启babel/plugin-transform-runtime为应用和依赖包启用语法辅助函数抽离module-builder关闭 ESM 语法的转换为app-builder做 tree-shaking 时提供必要前置条件通过这种方式我们就可以实现在构建过程中减少代码体积的最佳实践至于重复依赖的问题由于必定需要开发者介入做版本选择所以我们可以考虑在部署平台构建时自动上报 Dependency graph 数据然后由性能分析等平台将重复依赖的问题邮件抄送给相关开发者进行优化总结本文从构建工具的角度阐述了如何减少构建产物的体积。可以看到仅仅处理应用的构建是不够的为了实现最佳效果我们还需要介入公司内部依赖包的构建使依赖包的构建产物符合应用构建的需求。只有具备全场景的构建能力才能最大程度降低代码的构建体积。参考资料https://docs.npmjs.com/cli/v8/commands/npm-dedupehttps://babeljs.io/docs/en/babel-plugin-transform-runtimehttps://babeljs.io/docs/en/babel-preset-envhttps://babeljs.io/docs/en/babel-polyfillhttps://webpack.js.org/guides/tree-shaking/#roothttps://cdn.polyfill.io/v3/