关于前端:你构建的代码为什么这么大

3次阅读

共计 4854 个字符,预计需要花费 13 分钟才能阅读完成。

本文作者:文西

前言

代码体积的管制对前端来说至关重要,只管网络条件逐步变好,然而代码体积的减少不仅仅只影响资源加载速度,还会间接或间接影响浏览器各类性能指标。

例如减少用户内存应用耗费,内存的减少又会更频繁的触发 V8 引擎的 GC 机制,进而影响页面交互性能。

本文从一个典型的 Webpack+Babel 工程登程,找到构建产物体积变大的常见起因和对应的解决思路,缩小我的项目代码构建后的体积

<!– 本文从工程化的角度登程,帮忙咱们找到构建产物体积变大的常见起因和对应的解决思路,缩小我的项目代码构建后的体积 –>

Babel

babel 最常见的用处就是代码降级,使构建后的代码可能被低版本浏览器兼容,依照性能能够划分两局部

  1. API 降级
  2. 语法降级

通过 Babel 构建后的代码为了适配低版本浏览器通常会比源代码大上几倍,这外面除了源代码外还蕴含 API 垫片和语法辅助函数,别离对应上诉的 API 降级和语法降级,咱们看下如何缩小这部分的代码体积

core-js

💡 依照目前最新版本的 babel@7,@babel/polyfill 曾经废除,咱们应用 core-js 实现 API 的语法降级

core-js 能够为浏览器中可能不兼容的 API 提供垫片,例如 Promise,Map

import "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 的更优解,但理论过于现实

  1. 通常基于编译速度的思考,node_modules 下的模块不会参加 Babel 编译,仅参加 Webpack 打包,如果此时凑巧某个依赖包里没有申明所需的垫片,那么就可能呈现垫片缺失,最终导致线上环境 JS 运行异样。

    实际上这种状况在凌乱的 npm 生态中十分广泛,有不少 npm 包间接应用 tsc 打包,除非开发者手动染指,否则构建产物中就会短少 API 垫片,遇到这种状况往往只能在线上发现异常后手动增加依赖到 babel.include 中进行编译

  2. 并不是所有 JS 代码都会参加编译,例如通过一些平台动静下发的脚本,这些平台动静下发的代码齐全不通过编译,如果应用了未经降级的 api 也可能会呈现 JS 运行异样。

能够看到 entry,usage 都是存在问题的,所以也就有了平台化的计划,polyfill.io。

如果应用最新的现代化浏览器拜访该服务,那么返回的 JS 内容则是空的,反之它会响应浏览器所需的降级 API,既管制了包体积,也能确保未经编译的 JS 取得降级 API。

出于平安思考,咱们须要自部署服务,目前 polyfill.io 的 node.js 代码是齐全开源的,反对自部署,然而理论落地还须要思考缓存和异样兜底

@babel/runtime

core-js 是为了解决 API 降级问题存在的,然而咱们还有语法降级须要解决,例如 class,async

默认状况下 babel 为了实现 class 性能会生成一些内联辅助函数,例如下图的 createClass。这会产生一个问题,就是当多个模块都应用 class 语法时则会生成多个雷同的辅助函数,辅助函数不能复用

咱们能够通过注册 babel 插件 @babel/plugin-transform-runtime,将硬编码辅助函数的形式改为从 @babel/runtime 引入辅助函数,实现不同模块间辅助函数的复用

从下图能够看到 createClass 函数从硬编码改为require("@babel/runtime/helpers/createClass"),代码大幅放大

然而 @babel/plugin-transform-runtime 的计划也不是毫无问题,和 api 降级一样,同样面临各种依赖包构建不规范带来的困扰

最大的问题就是没有方法保障依赖包的产物肯定应用了 @babel/plugin-transform-runtime 进行构建,语法降级应用了内联的辅助函数,又或者应用了老版本的babel-runtime·,导致我的项目最终的构建产物对辅助函数进行了屡次打包

以绝对常见的依赖包构建工具 father-build 和 tsc 为例,他们都没有将语法辅助函数通过 @babel/runtime 依赖包进行提取,而是都以硬编码的模式存在每个 JS 模块当中。

这类由社区保护的 npm 包咱们不好解决,然而能够通过收敛公司外部构建工具的形式,对立解决公司外部保护的依赖包,使它们构建的产物合乎利用打包的需要,咱们在文章结尾处再说

Tree-shaking

tree-shaking 是缩小构建产物体积最无效的形式,以罕用 lodash 为例,g-zip 后的体积 24kb,然而我的项目中应用到的函数并不多,如果可能为它启用 tree-shaking,代码体积能管制在 1kb 以内

如何为依赖代码启用 tree-shaking?

  1. package.json 申明 module 字段,地址指向 ESM 标准的构建产物
  2. package.json 申明sideEffects:false,通知 Webpack 整个依赖包没有存在副作用,或者指明存在副作用模块的地址

ESM

ESM 相比 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-shaking,lodash 推出了反对 tree-shaking 的 lodash-es,antd@4 也不再须要装置 babel-plugin-import 插件,能够通过 tree-shaking 的形式原生反对代码按需加载,从而大幅放大构建体积

Duplicate dependencies 反复依赖

依赖反复打包是前端开发中的常见问题,容易呈现在公司外部长期无人保护的依赖包中

当咱们的我的项目中存在 Root→C→D@2.0.0,Root→B→D@3.0.0 相似的依赖关系时,node_module 构造如下

node_modules
  -- C <-- depends on D@2.0.0
  -- D@2.0.0
  -- B <-- depends on D@3.0.0
    -- node_modules
      -- D@3.0.0

能够看到在 node_modules 下嵌套装置了 2 个版本的依赖 D,即D@2.0.0D@3.0.0。这可能导致在构建的产物中也同样存在两份雷同依赖不同版本的代码,除了会影响代码体积,还可能导致代码运行异样

解决形式是降级 B 的依赖 D@2.0.0→D@3.0.0,此时重新安装后 node_modules 的嵌套构造会复原扁平

node_modules
  -- C <-- depends on D@3.0.0
  -- D@3.0.0
  -- B <-- depends on D@3.0.0

咱们能够应用 find-duplicate-dependencieswebpack-bundle-analyzer这些工具辅助咱们排查依赖反复打包的问题

最佳实际

回顾文章咱们对一个典型前端利用可能影响 Bundle 体积的因素进行了剖析,同时提出对应的解决方案。在文章的结尾咱们能够更进一步通过工程化和平台化的伎俩,以绝对一劳永逸的形式解决上诉问题

如下图,@company/app-builder负责构建利用,@company/module-builder负责构建依赖包,而后通过应用封装的 babel 配置@company/babel-base,对立解决 JS 编译

babel-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…
  • https://babeljs.io/docs/en/ba…
  • https://babeljs.io/docs/en/ba…
  • https://babeljs.io/docs/en/ba…
  • https://webpack.js.org/guides…
  • https://cdn.polyfill.io/v3/

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0