你构建的代码为什么这么大?如何优化~

大家好,我是若川。我持续组织了近一年的源码共读活动,感兴趣的可以 点此扫码加我微信 lxchuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有江西|湖南|湖北籍前端群,可加我微信进群。

7f643631344c81a18090eefdc3cefb46.jpeg

扫码加我微信 lxchuan12、拉你进源码共读


本文作者:文西

前言

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

例如增加用户内存使用消耗,内存的增加又会更频繁的触发 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 模块

741e40a99c4ec7dfc91c96016008ae53.png

@babel/preset-env根据项目中 browserlist 定义的用户环境,选择性插入垫片代码,减少垫片代码体积

在配置@babel/preset-env 时,useBuiltIns 属性非常重要,有两个值"entry"|"usage",分别为全量降级和按需降级

entry 全量降级

entry 非常直接,首先我们需要手动在代码的第一行import 'core-js',在执行编译时,会按照 browserlist 中定义的环境,把可能需要降级的 API 一次性插入并替换到 core-js 声明的位置

aa79ba28640b3dbe78ccaf443e25140f.png

开发者不再需要手动插入垫片,但这有个问题,即没有使用的 API 仍然会被打进 bundle 中,由于 ECMAScript 标准的不断发展,core-js 在 g-zip 压缩后也有 50kb 左右的体积,显然还是太大了

usage 按需降级

当选择 usage 时,babel 会扫描所有需要编译的 JS 代码,根据实际使用到的 API 选择性插入所需垫片

2de5fcb69af65bc9e40b35cb8f9f75fe.png

看起来是相比 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。

2ebf43ce8baf0907b7d9de289a458f63.png
Untitled

出于安全考虑,我们需要自部署服务,目前 polyfill.io 的 node.js 代码是完全开源的,支持自部署,但是实际落地还需要考虑缓存和异常兜底

@babel/runtime

core-js 是为了解决 API 降级问题存在的,但是我们还有语法降级需要解决,例如 class,async

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

68a04c2e158aa7a3635491fecc03d29a.png
Untitled

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

4405b33529682ba91b6440ef32b9e69c.png
Untitled

从下图可以看到 createClass 函数从硬编码改为require("@babel/runtime/helpers/createClass"),代码大幅缩小

1701386ba42c3a2fde9690c1aab386aa.png
Untitled

但是@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 编译

47208aed438e268593ef116b5e809b11.png
Untitled

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/commands/npm-dedupe

  • https://babeljs.io/docs/en/babel-plugin-transform-runtime

  • https://babeljs.io/docs/en/babel-preset-env

  • https://babeljs.io/docs/en/babel-polyfill

  • https://webpack.js.org/guides/tree-shaking/#root

  • https://cdn.polyfill.io/v3/


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274425.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用户体验需求层次_需求和用户体验

用户体验需求层次Shortly after the start of 2020 I led the development of a new website, and it went live in August. A week before the deployment, I paused development and took a step back in order to write about the project. Taking that pause, that step ba…

VMwareWorkstation设置U盘启动(或U盘使用)

最近在工作中&#xff0c;经常要和LINUX部署打交道&#xff0c;一般在生产环境部署之前需要在自己的机器上进行测试。比如使用U盘安装操作系统等。 在机器上安装了VMware Workstation9.0&#xff0c;运行多个测试虚拟机。理由所当然的要使用此做一些操作系统部署&#xff0c;…

类从未使用_如果您从未依赖在线销售,如何优化您的网站

类从未使用初学者指南 (A beginner’s guide) If you own a small business with a store front, you might have never had to rely on online sales. Maybe you’re a small clothing store or a coffee shop. You just made that website so people could find you online, …

狼书三卷终大成,狼叔亲传Node神功【留言送书】

大家好&#xff0c;我是若川。之前送过N次书&#xff0c;可以点此查看回馈粉丝&#xff0c;现在又和博文视点合作再次争取了几本书&#xff0c;具体送书规则看文末。众所周知&#xff0c;我在参加掘金人气作者打榜活动&#xff08;可点击跳转&#xff09;&#xff0c;需要大家投…

程序详细设计之代码编写规范_我在不编写任何代码的情况下建立了一个设计策划网站

程序详细设计之代码编写规范It’s been just over a month since MakeStuffUp.Info — my first solo project as an independent Creator; was released to the world. It was not a big project or complicated in any way, it’s not even unique, but I’m thrilled where …

偷偷告诉你们一个 git 神器 tig,一般人我不告诉TA~

大家好&#xff0c;我是若川。众所周知&#xff0c;我参加了掘金创作者人气作者投票活动&#xff0c;最后3天投票。今天可投28票&#xff0c;明天32票&#xff0c;后天36票&#xff08;结束&#xff09;。投票操作流程看这里&#xff1a;一个普通小前端&#xff0c;将如何再战掘…

DAO层使用泛型的两种方式

package sanitation.dao;import java.util.List;/** * * param <T>*/public interface GenericDAO <T>{/** * 通过ID获得实体对象 * * param id实体对象的标识符 * return 该主键值对应的实体对象*/ T findById(int id);/** * 将实体对象持…

将是惊心动魄的决战~

大家好&#xff0c;我是若川。一个和大家一起学源码的普通小前端。众所周知&#xff0c;我参加了掘金人气创作者评选活动&#xff08;投票&#xff09;&#xff0c;具体操作见此文&#xff1a;一个普通小前端&#xff0c;将如何再战掘金年度创作者人气榜单~。最后再简单拉拉票吧…

图书漂流系统的设计和研究_研究在设计系统中的作用

图书漂流系统的设计和研究Having spent the past 8 months of my academic career working co-ops and internships in marketing & communication roles, my roots actually stem from arts & design. Although I would best describe myself as an early 2000s child…

西里尔字符_如何设计西里尔字母Њ(Nje),Љ(Lje),Ћ(Tshe)和Ђ(Dje)

西里尔字符This article is about how to design Cyrillic characters Њ, Љ, Ђ, and Ћ (upright caps and lowercase; italics are not covered here). They are often problematic since they are Cyrillic, but not found in the Russian alphabet, so there is no much …

学习 vuex 源码整体架构,打造属于自己的状态管理库

前言这是学习源码整体架构第五篇。整体架构这词语好像有点大&#xff0c;姑且就算是源码整体结构吧&#xff0c;主要就是学习是代码整体结构&#xff0c;不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。其余四篇分别是&#xff1a;学习 jQuery 源码整体…

VMware workstation 8.0上安装VMware ESXI5.0

首先&#xff0c;在VMware的官网上注册&#xff0c;下载VMware ESXI的安装包vmware&#xff0d;vmvisor&#xff0d;installer&#xff0d;5.0.0&#xff0d;469512.x86_64.iso&#xff0c;它是iso文件&#xff0c;刻盘进行安装&#xff0c;安装过程中&#xff0c;会将硬盘全部…

最新ui设计趋势_10个最新且有希望的UI设计趋势

最新ui设计趋势重点 (Top highlight)Recently, I’ve spent some time observing the directions in which UI design is heading. I’ve stumbled across a few very creative, promising and inspiring trends that, in my opinion, will shape the UI design in the nearest…

学习 axios 源码整体架构,打造属于自己的请求库

前言这是学习源码整体架构系列第六篇。整体架构这词语好像有点大&#xff0c;姑且就算是源码整体结构吧&#xff0c;主要就是学习是代码整体结构&#xff0c;不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。学习源码整体架构系列文章如下&#xff1a;1.…

404 错误页面_如何设计404错误页面,以使用户留在您的网站上

404 错误页面重点 (Top highlight)网站设计 (Website Design) There is a thin line between engaging and enraging when it comes to a site’s 404 error page. They are the most neglected of any website page. The main reason being, visitors are not supposed to end…

学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

前言这是学习源码整体架构系列第七篇。整体架构这词语好像有点大&#xff0c;姑且就算是源码整体结构吧&#xff0c;主要就是学习是代码整体结构&#xff0c;不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。学习源码整体架构系列文章如下&#xff1a;1.…

公网对讲机修改对讲机程序_更少的对讲机,对讲机-更多专心,专心

公网对讲机修改对讲机程序重点 (Top highlight)I often like to put a stick into the bike wheel of the UX industry as it’s strolling along feeling proud of itself. I believe — strongly — that as designers we should primarily be doers not talkers.我经常喜欢在…

若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?

知乎问答&#xff1a;做了两年前端开发&#xff0c;平时就是拿 Vue 写写页面和组件&#xff0c;简历的项目经历应该怎么写得好看&#xff1f;以下是我的回答&#xff0c;阅读量5000&#xff0c;所以发布到公众号申明原创。题主说的2年经验做的东西没什么技术含量&#xff0c;应…

ui设计基础_我不知道的UI设计的9个重要基础

ui设计基础重点 (Top highlight)After listening to Craig Federighi’s talk on how to be a better software engineer I was sold on the idea that it is super important for a software engineer to learn the basic principles of software design.听了克雷格费德里希(C…

C# 多线程控制 通讯 和切换

一.多线程的概念   Windows是一个多任务的系统&#xff0c;如果你使用的是windows 2000及其以上版本&#xff0c;你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢&#xff1f;当一个程序开始运行时&#xff0c;它就是一个进程&#xff0c;进程所指包括运行中…