- 原文地址:alistapart.com/article/res…
- 原文做者:Jeremy Wagner
- 译者:马雪琴
- 声明:本翻译仅作学习交流使用,转载请注明来源
你和开发团队的成员热情游说老板赞成对公司的老网站进行全面的重构,大家的请求被管理层甚至是最高管理层都听到了,他们赞成了。高兴之余,你和团队开始与设计、IA 等团队一块儿工做。没过多久,大家就写出了新代码。javascript
重构工做一开始很是简单,就是处处安装 npm,这其实就是在快速安装生产依赖项,就像一个大学生在作桶支架,而不关心次日早上的状况同样。css
而后,你就启动了。html
与大多数豪饮的后果不一样,痛苦并非次日早上就开始的。可是……几个月后,产品全部者和中层管理人员开始感到恶心和头痛,他们想知道为何产品推出以来,转化率和收入都降低了。而后事情会恶化到极点,CTO 周末从度假小屋回来,质问为何网站加载速度如此之慢——若是它真的加载过。java
重构时每一个人都很开心,重构后没有人快乐了。欢迎来到你的第一个 “JavaScript 宿醉”。node
当你与严重的“宿醉”做斗争时,“我告诉过你”这句话将是你应得的,它表明了激怒和指责——假设你还能够在如此糟糕的状态下战斗。react
说到 ”JavaScript 宿醉”,不少人要为此承担责任,但相互指责只是在浪费时间。当今的网络环境要求咱们拥有比竞争对手更快的迭代速度,这种压力驱使咱们可能会利用任何可用的手段来尽量地提升生产力,所以,咱们更有可能(但也不必定)构建出开销更大的应用程序,并可能会使用影响性能和可访问性的开发模式。webpack
Web 开发并不容易,它是一个漫长的过程,咱们不多在第一次尝试时就取得成功。然而,web 工做最好的地方也在于,咱们没必要一开始就把它弄得很完美,咱们能够在过后进行改进,这正是本系列的第二部分的目的所在。要达到完美还有很长的路要走,如今,让咱们在短时间内经过改进站点的脚原本减弱 “JavaScript 宿醉”。git
基本的优化列表可能看起来很机械,可是值得一试。大型开发团队,特别是那些跨多个库工做,或不使用优化样板文件的团队,很容易忽略这些。es6
首先,确保您的工具链配置了 tree shaking。若是你对 tree shaking 还不熟悉,我去年写了一篇 tree shaking 指南,你能够参考一下。简而言之,tree shaking 是指将代码库中未使用的代码再也不打包到生产包中的过程。github
现代的一些打包工具,如 webpack, Rollup 以及 Parcel 都有现成的 tree shaking 功能。Grunt 和 Gulp 只是任务运行器,并非打包工具,因此它们没有 tree shaking。任务运行器不会像打包工具那样构建一个依赖关系图,相反,它们根据提供的配置文件,用许多的插件来执行离散的任务。任务运行器可使用插件进行扩展,因此你能够经过绑定打包工具来处理 JavaScript。若是这种方式对你来讲存在问题,那么你可能就须要手动审计并删除未使用的代码。
要想让 tree shaking 生效,须要知足下面几个条件:
Tree shaking 在构建过程当中不太可能没有做用,若是真的没有,那就让它发挥做用。固然,它的有效性也因状况而异,它还取决于你导入的模块是否会引入反作用,这些反作用可能会影响打包工具删除未使用的导出模块。
你颇有可能正在使用某种形式的代码拆分,可是使用的方式值得从新评估。不管你如何拆分代码,有两个问题必定须要注意:
这些都很重要,由于减小冗余代码对性能相当重要。延迟加载能够经过减小页面初始 JavaScript 大小来提升性能。使用诸如 Bundle Buddy 之类的分析工具能够帮助你发现是否存在代码冗余问题。
在考虑延迟加载时,很难知道从哪里开始。当我在现有项目中寻找时,我会在整个代码库中搜索用户交互点,例如单击和键盘事件,以及相似的候选项。任何须要用户交互才能运行的代码均可能是动态加载的好的选择。
固然,按需加载脚本可能会显著延迟交互性,由于必须先下载交互所需的脚本。若是不关心数据使用状况,能够考虑使用 rel=prefetch 资源提示以较低的优先级加载这些脚本,这些脚本就不会与关键资源争用带宽。rel=prefetch 的支持度很好,而且即便浏览器不支持它,也不会有任何问题,由于浏览器会忽略它们不理解的标记。
理想状况下,你应该尽量多地自托管站点的依赖项。若是因为某种缘由必须从第三方加载依赖项,请在打包工具的配置中将它们标记为外部包,不然可能会致使你网站的访问者将从本地以及从第三方托管下载相同的代码。
让咱们来看一个可能会出现的假设状况:假设你的站点从公共 CDN 加载 Lodash,你还在本地开发的项目中安装了 Lodash,可是,若是你没有将 Lodash 标记为外部的,那么你的产品代码最终将加载它的第三方副本,而不是绑定的本地托管副本。
若是你了解你的代码块,这彷佛只是一个常识,但我看过这一常识被开发者忽视,这的确是值得你花时间检查确认的一件事情。
若是你不相信能够自行托管第三方依赖项,那么能够考虑为它们添加 dns-prefetch、preconnect 甚至 preload 提示。这样能够减小站点的交互时间,若是 JavaScript 对呈现内容相当重要,则能够减小站点的速度指数。
Userland JavaScript 就像一个大得使人发指的糖果店,咱们做为开发人员,对大量的开源产品感到十分敬畏,框架和库容许咱们快速扩展应用程序,实现原本须要花费大量时间和精力的各类各样的功能。
虽然我我的倾向于在项目中尽可能减小客户端框架和库的使用,但它们的价值是引人注目的。然而,咱们确实有责任在咱们安装的东西上采起强硬的态度,当咱们构建并交付了一些依赖于大量已安装代码来运行的东西时,就表明咱们接受了只有这些代码维护者才能实际去解决一些问题,对吧?
多是也可能不是,这取决于所使用的依赖项。例如,React 很是流行,但 Preact 是一个很是小的替代品,它基本上拥有和 React 相同的 API,并与许多 React 插件兼容。Luxon 和 date-fns 比 moment.js 更简洁,但也不是很小。
像 Lodash 这样的库提供了许多有用的方法,然而,其中一些很容易被原生 ES6 取代。例如,Lodash 的 compact 方法能够替换为 filter 数组方法。咱们其实并不不须要引入大型工具库,咱们能够轻松地替换更多。
不管你喜欢的什么样的工具,思想都是同样的:作一些研究,看看是否有更小的选择,或者原生的语言特性是否就能够达到这个目的。你可能会惊讶地发现,要真正地减小应用程序的开销其实很简单。
你颇有可能在工具链中使用 Babel 将 ES6 源代码转换为能够在传统浏览器上运行的代码,这是否意味在传统浏览器彻底消失以前,咱们必定要给根本不须要它们的浏览器提供巨大的代码包?固然不是!差别服务经过将 ES6 源码生成两个不一样版本的代码包,能够帮助咱们解决这个问题:
实现这一点有点复杂,我写了一种实现方法,在这里就不深究了,简而言之就是,你能够修改构建的配置来生成一份额外的更小版本的代码包,而且只提供给现代浏览器。最重要的是,这些都是能够在不牺牲任何特性或功能的状况下实现的节省。视你的应用程序代码而定,节省的成本可能会至关可观。
将这些包提供给对应平台的最简单模式以下,它在现代浏览器中也很好用:
<!-- 现代浏览器加载这份文件: --> <script type="module" src="/js/app.mjs"></script> <!-- 传统浏览器加载这份文件: --> <script defer nomodule src="/js/app.js"></script> 复制代码
不幸的是,这种模式有一个警告:像 IE 11 这样的传统浏览器,甚至像 Edge 15 到 18 这样相对现代的浏览器,都会同时下载这两个包。若是这对你来讲是能够接受的,那就没有问题。
若是你担忧传统浏览器下载两组包有性能问题,那么你须要找一个解决方案。这里有一个潜在的方案,即便用脚本注入(而不是上面的脚本标签)来避免在受影响的浏览器上重复下载:
var scriptEl = document.createElement("script"); if ("noModule" in scriptEl) { // 设置现代脚本 scriptEl.src = "/js/app.mjs"; scriptEl.type = "module"; } else { // 设置传统脚本 scriptEl.src = "/js/app.js"; scriptEl.defer = true; // type="module" 默认会延迟, 这里须要手动设置。 } // 注入! document.body.appendChild(scriptEl); 复制代码
这段脚本推断若是一个浏览器在脚本元素中支持 nomodule 属性,它就能解析 type="module"。这确保了传统浏览器只能加载获得传统脚本,而现代浏览器只能加载获得现代脚本。可是须要注意的是,动态注入的脚本默认状况下是异步加载的,因此若是依赖顺序很重要,那么须要将 async 属性设置为 false。
个人意思并非说要直接废弃 Bable,它是必不可少的,可是天哪,它在你不知道的状况下增长了不少额外的东西。检查一下它转换的代码是有好处的。在你的编程习惯上作一些小的改变就会对 Babel 的输出产生积极的影响。
默认参数是一个很是方便的 ES6 功能,你可能已经使用过:
function logger(message, level = "log") { console[level](message); } 复制代码
这里须要注意的是 level 参数,它的默认值是“log”。这意味着若是咱们想用这个函数调用 console.log,咱们不须要指定 level 参数。太好了,对吧?但 Babel 转换这个函数时,输出以下:
function logger(message) { var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log"; console[level](message); } 复制代码
这是一个例子:尽管咱们的初衷是好的,但开发人员的便利可能会拔苗助长。源代码中仅有的几个字节如今已经在生产代码中转换为更大的字节。代码丑化对此也无能为力,由于 arguments 没法压缩掉。哦,不要认为 rest 参数可能会是一种更好的解决方案,实际 Babel 将它们转换得更加庞大:
// 源码 function logger(...args) { const [level, message] = args; console[level](message); } // Babel 输出 function logger() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } const level = args[0], message = args[1]; console[level](message); } 复制代码
更糟糕的是,Babel 甚至对 @babel/preset-env 配置了针对现代浏览器的项目也转换了这段代码,这意味差别服务中的 JavaScript 现代包也会受到影响!你可使用 loose transforms 来解决这一漏洞——这是一个好主意,由于它们一般比那些更符合规范的转换包要小得多——可是,若是你稍后从构建管道中删除 Babel,启用 loose transforms 可能会致使问题。
不管你决定是否启用 loose transforms,这里有一种方法能够去掉置换的默认参数:
// Babel 不会转换它 function logger(message, level) { console[level || "log"](message); } 复制代码
固然,默认参数并非惟一须要警戒的特性。例如,展开语法会被转换,箭头函数和其它一大堆东西也会被转换。
若是你不想彻底避免使用这些功能,如下几个方法能够减小它们的影响:
这只是我我的的见解,但我认为最好的选择是彻底避免对为现代浏览器生成的包进行代码转换。但这不必定可行,若是你使用了 JSX,它就必须针对全部浏览器进行转换,或者若是你使用的是不被普遍支持的前沿语言特性。后一种状况中,咱们有必要问一下,这些功能对于提供良好的用户体验是否真的是必需的(它们不多是必需的)。若是你认为必定要使用 Babel,那么你应该时不时地去看看它转换的内容,看看 Babel 可能会作哪些事情,你是否能够进行改进。
当你按摩你的太阳穴,想知道这个可怕的 “JavaScript 宿醉”何时才会消失,你要知道,正是当咱们急于获得一些东西的时候,用户体验才会受到影响。因为 web 开发社区热衷于以竞争的名义进行更快的迭代,因此你有必要稍微放慢速度。你会发现,这样作可能会使你的迭代速度不如竞争对手,可是你的产品将比他们的更快。
当你把这些建议应用到你的代码库中时,要知道进步不是一晚上之间天然发生的。Web 开发是一项工做。真正有影响力的工做是在咱们深思熟虑并致力于长期的工艺时完成的。专一于稳定的改进,度量、测试、重复,你的站点的用户体验将获得改善,而且随着时间的推移,你将一点一点地加快速度。
若是你以为这篇内容对你有价值,请点赞,并关注咱们的官网和咱们的微信公众号(WecTeam),每周都有优质文章推送: