阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

Dropbox 瘦身攻略:我们如何把 JavaScript 包缩小三分之一

作者 | Umair Nadeem,Rich Hong

  • 2023-09-19
    北京
  • 本文字数:4622 字

    阅读完需:约 15 分钟

大小:2.45M时长:14:16
Dropbox 瘦身攻略:我们如何把JavaScript包缩小三分之一

不知道各位朋友是否还记得,上一次正打算点击网站上的按钮、结果页面突然变化导致你点上了错误的位置是什么时候。或者说,上一次你因为实在忍受不了缓慢的加载速度而愤然点叉又是在什么时候?

 

这些问题在如今内容愈发丰富、交互度越来越高的应用场景中被无限放大。为了支持更复杂的功能,我们不得不编写出更多的前端代码,导致浏览器端需要接收、解析和执行的字节更多,最终性能自然变得更差。

 

在 Dropbox,我们深深了解此等糟糕体验是多么令人崩溃。所以过去一年来,我们的 Web 性能工程团队抽丝剥茧、将性能问题溯源到了一个常常被忽视的元素身上:模块捆绑器。

 

米勒定律认为,人脑在任何给定的时间内只能容纳一定量的信息,所以大部分现代代码库(包括我们 Dropbox 的代码库)才会被拆分成一个个更小的模块。模块捆绑器负责把应用程序中的各类组件(例如 JavaScript 和 CSS)合并成捆绑包,并在页面加载时由浏览器下载这些捆绑包。最常见的处理方式就是将捆绑包保存为最小 JavaScript 文件的形式,用以存放 Web 应用程序中的大部分逻辑。

 

Dropbox 模块捆绑器的首次迭代设计于 2014 年,当时以性能为先的模块捆绑方法才刚刚兴起(分别在 2012 年和 2015 年由 Webpack 和 Rollup 率先提出)。但毕竟年代久远,那时候的方案跟现代设计比起来还是太过简陋。我们的模块捆绑器并没多少性能优化,使用起来比较繁琐,既影响用户体验又会拖慢开发速度。

 

随着捆绑器逐渐显露老态,我们决定面向未来做好性能优化、全面替换掉这位应当功成身退的老将。当前也是替换的最佳时机,因为我们正好在着手将页面迁移至 Edison(我们的全新 Web 服务栈),统筹规划有望一箭双雕。替换之后,我们的静态资产管线也将迎来更现代的捆绑器,在架构层面让集成更为简单。

现有架构


虽然我们原本的捆绑器拥有相对较快的构建速度,但也存在着不少短板,包括捆绑包太过臃肿、工程师们感到难以维护等等。工程师们只能手动定义把哪些脚本跟包捆绑在一起,而且我们之前只简单提供页面渲染所需要的包,但几乎未做任何性能优化。随着时间推荐,这种粗糙的方案也带来了以下几大显著问题。

 

问题一:捆绑代码有好几个版本

 

直到不久前,我们还在使用名为 Dropbox Web Server(DWS)的自定义 Web 架构。简单来讲,每个页面都由多个小页(pagelet,即页面中的子部分)组成,因此导致每个页面都有多个 JS 入口点,而各 servlet 也由后端处对应的控制器提供服务。虽然这种部署在多个团队同时处理同一页面时速度更快,但也往往导致 pagelet 指向不同的后端代码版本。这就要求 DWS 能支持在同一页面上交付不同版本的打包代码,而这经常会引发一致性问题(例如在同一页面上加载相同单例的多个实例)。我们向 Edison 的迁移将消除这种 pagelet 架构,从而更灵活地采取更符合行业标准的捆绑方案。

 

问题二:需要手动分割代码

 

所谓代码分割,就是把 JS 包分割成更小的块的过程,这样浏览器就能只加载当前页面所需要的代码库部分。例如,假设用户先访问 dropbox.com/home,而后访问 dropbox.com/recent,那么如果不进行代码分割,则浏览器会下载整个 bundle.js,这无疑将显著减慢页面的初始导航速度。



所有页面的全部代码均通过单一文件提供

 

但在代码分割之后,浏览器只需要下载页面所需要的各个代码块。由于浏览器下载的代码量更少,所以 dropbox.omc/home 的初始导航速度将大大提升。此外,代码分割可以保证先加载关键脚本,而后再异步加载、解析和执行非关键脚本。共享代码片段也将被浏览器缓存下来,进一步减少用户在不同页面间移动时所需下载的 JS 代码量。所有这些,都将大大减少 Web 应用程序的加载时间。



仅下载页面所需的新代码块

 

由于我们现在的捆绑器没有任何内置的代码分割工具,所以工程师只能手动对包做定义。具体来讲,我们的打包 map 是个 6000 多行的庞大字典,具体指定了哪些模块该放进哪个包中。

 

可以想见,随着时间推移这样一套架构的维护工作将变得异常复杂。为了避免非优打包,我们强制执行了一套严格的测试(打包测试),但因为每次变更都可能打乱原本的模块排列,所以工程师们变得神经紧张、苦不堪言。

 

这也导致我们的实际代码量比页面所需要多得多。例如,假定我们有以下包 map:

 

{  "pkg-a": ["a", "b"],  "pkg-c": ["c", "d"],}
复制代码

 

如果页面依赖于模块 a、b 和 c,则浏览器只须进行两次 HTTP 调用(分别获取 pkg-a 和 pkg-b),而非对各模块各进行一次(共三次)调用。这虽然会减少 HTTP 调用的开销,但同时也会加载不必要的模块——在本示例中就是模块 d。由于缺乏摇树优化,我们不但加载了不必要的代码,还加载了页面不需要的整个模块,因此会拖慢整体用户体验。

 

问题三:缺少摇树优化

 

摇树是一种包优化技术,能够消除未使用的代码来帮助捆绑包瘦身。假设我们的应用程序需要导入包含多个模块的第三方库,如果没有摇树优化,则实际加载的大部分捆绑代码其实都毫无用处。



无论是否实际使用,所有代码都会被捆绑进来

 

通过摇树优化,我们可以分析代码的静态结构,并删除一切未被其他代码直接引用的代码。这样最终的捆绑包就能更加精简小巧。



只捆绑要使用的代码

 

因为我们之前的捆绑器不太完善,所以其中没有任何摇树功能。生成的包往往包含大量未使用代码,特别是来自第三方库的代码,这会导致页面加载无用内容、延长等待时间。此外,因为我们使用 protobuf 定义来实现从前端到后端的高效数据传输,所以在检测某些可观察性指标时往往要引入高达几 MB 的未使用代码!

为何选择 Rollup


多年来我们其实考虑过不少解决方案,并最终把核心需求梳理了出来:我们真正需要的,就只有自动代码分割、摇树优化,以及可以进一步优化捆绑管线的可选插件。Rollup 就是当前最成熟、也能灵活融入到我们现有构建管线的工具,于是最终成为我们的首选解决方案。

 

另一个原因是:有助于降低工程开销。因为我们已经在使用 Rollup 捆绑我们的 NPM 模块,所以继续扩大 Rollup 的使用范畴肯定比再引入新工具要划算得多。此外,这也意味着跟其他捆绑器相比,我们已经在之前的运营中掌握了更多关于 Rollup 特性的工程专业知识,能有效降低用不下去的可能性。最后,我们还算了一笔账,发现跟深入集成 Rollup 相比,在原有模块捆绑器中重现 Rollup 的功能需要投入更多工程资源。

不负众望的 Rollup


我们都知道,安全、分步推出模块捆绑器绝非易事,毕竟我们在期间需要同时可靠支持两种模块捆绑器(并生成两种对应的捆绑包)。我们主要关心的问题包括如何保证捆绑代码稳定、无 bug,如何增加构建系统和 CI 的负载,还有怎样激励团队接受在其页面中使用 Rollup 捆绑包。

考虑到可靠性和可扩展性等问题,我们把发布过程分成了四个阶段。


  • 开发者预览阶段:允许工程师在开发环境中选择加入 Rollup 捆绑包。这样我们就能让开发者尽早发现 Rollup 捆绑包引发的任何意外,借此推动行之有效的众包 QA 测试。认真收集相关信息后,我们将有充足的时间解决 bug、适应范围变更。

  • 面向 Dropbox 员工的内部预览阶段将全面推广 Rollup 捆绑包,借此收集早期性能数据并进一步获取关于应用程序行为变化的实践反馈。

  • 通用阶段,即逐步向所有 Dropbox 用户(包括内部和外部用户)推出 Rollup 捆绑包。在此之前,我们已经对 Rollup 包做过彻底测试并确定其稳定性已经达到较高水平。

  • 维护阶段,强调解决项目中遗留的所有技术债,再通过迭代让 Rollup 进一步优化性能和开发者体验。我们意识到,如此规模的大体量项目将不可避免地积累下一些技术债,我们应计划在某个阶段将其解决,而不能假装债务不存在。

 

为了有效支持各个阶段,我们混合使用了基于 cookie 的门控和内部功能门控系统。以往,Dropbox 的大多数部署都纯粹借助我们的内部功能门控系统得以完成。但这一次,我们决定允许基于 cookie 的门控在 Rollup 和旧捆绑包间快速切换,从而加快调试速度。每个发布阶段都以交替形式分步推出,包括从 1%、10%、25%,到 50%乃至最终的 100%。这让我们能够灵活地收集早期性能与稳定性结果,当发现问题时进行无缝回滚,同时尽可能降低对内、外部用户造成的影响。

 

因为我们需要迁移大量页面,所以除了建立安全可靠的 Rollup 切换策略之外,还得激励页面所有者主动执行切换。由于我们的 Web 栈将配合 Edison 进行一波重大改造,所以这应该是个可以一箭双雕的绝佳时机。如果把 Rollup 塑造成 Edison 所支持的独特功能,那开发团队应该会更愿意同时接受 Rollup 和 Edison,我们也能借此将 Rollup 的迁移策略跟 Edison 升级紧密绑定起来。

 

Edison 也有望借此提高自己的性能和开发速度。我们认为,将 Edison 与 Rollup 相结合,会在整个公司内产生强烈的转型协同效应。

挑战与障碍


我们早就做好了迎接意外挑战的准备,但事实证明将一种构建系统(Rollup)跟另一种构建系统(基于 Bazel 的原有基础设施)进行复杂对接,其挑战性要远远大于我们的任何想象。

 

首先,我们发现同时运行两种不同模块捆绑器,所消耗的资源要远超我们的估计。Rollup 的摇树算法虽然相当成熟,但仍需要将所有模块都先加载到内存中,之后生成分析关系并摇出代码所需的抽象语法树。此外,我们将 Rollup 集成到 Bazel 中的作法,限制了我们缓存中间构建结果的能力。也就是说,我们需要持续集成以重建并重新缩小每个构建上的全部 Rollup 块。这导致我们的持续集成构建因内存耗尽而超时,显著拖慢了部署节奏。

 

我们还发现了 Rollup 摇树算法中的几个 bug,这会导致摇树优化过于激进。值得庆幸的是问题不大,我们在开发者预览阶段就将其修复,所以最终用户并未受到影响。此外,我们发现旧版捆绑程序会提供来自第三方库的某些代码,而这些代码与 JS 严格模式并不兼容。一旦将这些代码提交给采用严格模式的新捆绑器,则会在浏览器中引发极为严重的运行时错误。这就要求我们对整个代码库、特别是与严格模式不兼容的补丁代码,开展一轮全面审计。

 

最后,在 Dropbox 内部员工预览阶段,我们发现 Rollup 和旧版捆绑器之间的 A/B 遥测指标并未体现出符合预期的 TTVC 性能提升。我们最终意识到,这是因为 Rollup 生成的代码块比旧版捆绑器生成的代码块要多得多。尽管我们最初假设 HTTP2 的多路复用能消除大量代码块引发的性能下降,但事实证明代码块过多还是会导致浏览器耗费更长的时间来获取页面所需的各模块。再有,模块数量的增加也会拉低压缩效率,因为 Zlib 等压缩算法使用的是滑动窗口方法执行压缩,就是说单一大文件的压缩效率要明显好于多个小文件。

最终结果


在向全体 Dropbox 用户推出 Rollup 之后,我们发现新项目将 JavaScript 包缩小了约三分之一,JS 脚本总量减少了 15%,TTVC 也实现了适度改进。我们还通过自动代码分割显著提高了前端开发速度,开发人员现在不必在每次变量时都手动调整捆绑包定义。最后,也可能是最重要的一点在于,我们完成了捆绑基础设施的现代化改造,削减了自 2014 年以来积累的大量技术债,显著减轻了未来的项目维护负担。

 

除了令人眼前一亮的实践表现之外,Rollup 项目还帮助我们发现了现有架构中的几个瓶颈:例如多个渲染会阻塞 RPC,对第三方库的函数调用过多,以及浏览器加载模块依赖性 map 效率太低等。凭借 Rollup 丰富的插件生态系统,解决原有代码库中此类瓶颈正变得越来越简单。

总而言之,全面采用 Rollup 作为模块捆绑器不仅给性能和生产力带来立竿见影的提升,也将在未来帮助 Dropbox 实现更为显著的性能改进。

 

原文链接:

https://dropbox-tech.translate.goog/frontend/how-we-reduced-the-size-of-our-javascript-bundles-by-33-percent

相关阅读:

最佳的 18 个 JAVASCRIPT 前端开发框架和库

新一波 JavaScript Web 框架

JavaScript 框架大战已结束,赢家只有一个

跨过四个时代,JavaScript框架终于可以与原生应用SDK竞争了

2023-09-19 12:576473

评论

发布
暂无评论
发现更多内容

终于有人把操作系统、网络系统、线程进程、IO模型全部总结出来了

程序知音

Java 后端 操作系统 网络 TCP/IP

科创人·观远数据CEO苏春园:让业务用起来,是BI行业推倒渗透率之墙的关键

科创人

大数据

NFTScan 与 Port3 在 NFT 数据领域达成战略合作

NFT Research

前两天面了个腾讯拿 38K 出来的,让我见识到了基础的天花板,今天share给大家~

程序知音

Java 程序员 java面试 后端技术 八股文

2022亚洲国际物联网展会

AIOTE智博会

物联网展览会

2022长三角工业自动化展会将于10月在南京国际展览中心召开

AIOTE智博会

工业自动化展会 工业机器人展会 江苏工博会

Python 爬虫 JS 逆向 X-Bogus,signature 加密算法,AST 理论篇

梦想橡皮擦

Python 爬虫 7月月更

如何搭建一个好的知识库管理系统?

Geek_da0866

JS数组方法

bo

JavaScript 前端 7月月更

Linux 文件系统函数

贾献华

7月月更

国产统信UOS系统运行小程序的探索

Geek_99967b

小程序

SCA在得物DevSecOps平台上应用​

得物技术

安全 DevSecOps SCA

企业进行知识管理有哪些好处?

Geek_da0866

百度APP Android包体积优化实践(二)Dex行号优化

百度Geek说

Java Andriod

设计消息队列存储消息数据的 MySQL 表格

Fan

架构实战营

【等保小知识】等保整改是什么意思?整改内容包括哪些?

行云管家

等保测评 等级测评 等保整改

2022年数据库审计产品排行榜-必看!

行云管家

数据库 数据库审计

JAVA编程规范之异常处理

源字节1号

微信小程序 软件开发 前端开发 后端开发

学界VS工业界:深度学习究竟能不能打破视频编解码天花板

小红书技术REDtech

深度学习 音视频 视频编解码 音视频技术

首发!这份字节大佬亲码算法面试大厂进阶宝典,让你轻松刷爆LeetCode!

了不起的程序猿

字节跳动 算法 java程序员

strcmp() - 比较字符串

謓泽

7月月更

长期的远程工作面临的几个问题和持续改进的组织自动化

Ryan Zheng

远程办公

Idea 连接 MySQL 数据库

攻城狮杰森

MySQL IDEA database 7月月更

对话ACE第四期:分布式数据库未来发展的挑战和机遇

OceanBase 数据库

数据库 分布式数据库 oceanbase

真人踩过的坑,告诉你避免自动化测试新手常犯的10个错误

禅道项目管理

自动化 测试 自动化测试

乘数科技云管控平台适配阿里云PolarDB,共促云原生数据库生态繁荣

阿里云数据库开源

数据库 阿里云 开源数据库 polarDB PolarDB for PostgreSQL

今日分享| 阿里巴巴内部最新Docker精髓笔记

冉然学Java

Java Docker 阿里 #技术干货# Java 开发

K8S多集群管理很难?试试Karmada | K8S Internals系列第3期

BoCloud博云

容器 容器云 K8s 多集群管理

C# 使用Timer和ProgressBar控件制作一个倒计时器

IC00

C# 7月月更

仅需一个依赖给Swagger换上新皮肤,既简单又炫酷

程序知音

Kyligence 出席华为全球智慧金融峰会,加速拓展全球市场

Kyligence

数据湖 数据分析 OLAP

Dropbox 瘦身攻略:我们如何把JavaScript包缩小三分之一_架构_InfoQ精选文章