速来报名!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:576711

评论

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

苹果电脑删除磁盘分区及双系统分区的办法

互联网搬砖工作者

Double-check 技术:Golang 中多线程编程的必备技能

Jack

golang 设计模式

不会PS没关系,AI拼图技术已能以假乱真|斯坦福研究

Openlab_cosmoplat

开源社区 ps

你的留言,我们都收到了

OceanBase 数据库

数据库 oceanbase

亚信科技AntDB数据库荣获互联网周刊金i奖“2022年度产品”

亚信AntDB数据库

数据库 AntDB 国产数据库 AntDB数据库 企业号 4 月 PK 榜

YonTalk 大咖论道:YonBuilder 低代码开发平台能力解析

YonBuilder低代码开发平台

Mac OS如何显示隐藏文件和文件扩展名

互联网搬砖工作者

深耕智能边缘研究和应用,英特尔中国研究院、南京英麒联合探索算力前沿

科技热闻

卡奥斯赋能发展引擎,“工赋山东”再加“数”!

Openlab_cosmoplat

工业互联网 开源社区

软件测试/测试开发丨4步,用 Docker搭建测试用例平台 TestLink

测试人

Docker 软件测试 自动化测试 测试开发 testlink

阿里云加入“一云多芯”应用创新计划,首批通过金融专有云能力评估

云布道师

混合云

让ChatGPT手把手教我们学操作系统是一种怎样的体验?

Java全栈架构师

程序员 AI 后端 操作系统 计算机

亮相数字化转型大会!卡奥斯助力两化融合工业转型!

Openlab_cosmoplat

工业互联网 开源社区

迪士尼的“元宇宙梦”醒了

Openlab_cosmoplat

开源社区 元宇宙

靠近用户侧和数据,算网融合实现极致协同

阿里云CloudImagine

云计算 边缘计算 边缘云

开心档之Go 错误处理

雪奈椰子

Feast on Amazon 解决方案

亚马逊云科技 (Amazon Web Services)

人工智能

格式塔理论

Data 探险实验室

可视化 大屏可视化 可视化看板 大屏布局 仪表板

焱融科技荣获爱分析·信创产品及服务创新奖

焱融科技

#高性能 #分布式文件存储 #文件存储 #全闪存储 #容器存储

软件测试/测试开发丨做web自动化时,定位元素常用方法有哪些?

测试人

软件测试 自动化测试 测试开发 Web自动化测试

ChatGPT已能操控机器人,工程师连代码都不用写,网友:微软在搞天网?

Openlab_cosmoplat

人工智能 机器人 开源社区 ChatGPT

智慧公厕解决方案,光明源方案揭秘

光明源智慧厕所

智慧城市

前端培训学习的就业前景怎么样

小谷哥

如何当个优秀的文档工程师?从 TC China 看技术文档工程师的自我修养

NebulaGraph

技术文档

如何通过Java代码在PowerPoint 幻灯片中插入公式

在下毛毛雨

PowerPoint 公式 java‘

一次偶然机会发现的MySQL“负优化”

做梦都在改BUG

Java MySQL 数据库 性能优化

The Foundry Modo 16 Mac版(专业的三维建模软件)

Rose

mac软件下载 Foundry Modo 三维建模软件

国内外低代码开发平台优劣势一览

YonBuilder低代码开发平台

开心档之Go 语言环境安装

雪奈椰子

理性探讨AIGC未来的发展方向

加入高科技仿生人

人工智能 低代码 AIGC

喜报:旺链科技成为龙芯生态重要合作伙伴

旺链科技

区块链 生态合作

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