未使用的 JavaScript 会拖慢页面加载速度,主要体现在以下几点:
▪ 如果 JavaScript 是渲染阻塞的,则浏览器必须下载、解析、编译和执行脚本,然后才能继续渲染页面所需的所有其他工作。
▪ 即使 JavaScript 是异步的(不是渲染阻塞),代码在下载时也会与其他资源竞争带宽,这会对性能产生重大影响。 在昂贵的手机流量费用面前,通过网络发送未使用的代码也是一种浪费。
如何删除未使用的代码
查找未使用的代码
Chrome DevTools 中的coverage选项卡可以为开发者提供未使用代码的逐行细分。 Puppeteer 中的 Coverage 类也可以帮助自动执行检测未使用代码和提取已使用代码的过程,比如下面的代码:
// 启动 JavaScript 和 CSS coverage分析
await Promise.all([
page.coverage.startJSCoverage(),
page.coverage.startCSSCoverage(),
]);
// 导航到一个页面
await page.goto('https://example.com');
// 禁止 JavaScript 和 CSS coverage分析
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage(),
]);
let totalBytes = 0;
let usedBytes = 0;
const coverage = [...jsCoverage, ...cssCoverage];
for (const entry of coverage) {
totalBytes += entry.text.length;
for (const range of entry.ranges) usedBytes += range.end - range.start - 1;
}
console.log(`Bytes used: ${(usedBytes / totalBytes) * 100}%`);
在浏览器中,可以通过下面的方法查找未使用的代码:
▪ 当 DevTools 处于焦点状态时,按 Command+Shift+P (Mac) 或 Control+Shift+P(Windows、Linux、ChromeOS)可打开命令菜单,输入coverage。
▪ 选择Show Coverage
▪ 单机录制按钮,Coverage选项卡提供了浏览器加载的每个文件使用了多少 CSS(和 JavaScript)的概览。
绿色代表使用的 CSS,红色代表未使用的 CSS。
▪ 单击 CSS 文件可在上面的预览中查看其使用的 CSS 的逐行细分
在上面的屏幕截图中,devsite-google-blue.css 的第 55 至 57 行和 65 至 67 行未使用,而第 59 至 63 行已使用。
支持删除未使用代码的构建工具
查看以下 Tooling.Report 测试,了解打包程序是否支持删除未使用的代码的功能:
代码分割
代码分割是交付高性能 JavaScript 应用程序的重要组成部分,有助于避免下载和执行超出给定页面所需的 JavaScript。 从更高的层次上来说,“代码拆分”是指将打包的代码分解为多个较小的 Bundle 包的过程,这些 Bundle 包可以根据需要独立加载和执行。
死代码消除(Unused Code Elimination)
死代码消除是删除当前应用程序未使用的代码的过程。 代码被解析以创建一个抽象语法树,然后遍历该树以查找未使用的函数和变量,最后该树被转换回 JavaScript 源代码。
有许多工具可以在 JavaScript 源代码上执行死代码消除,其中最流行的是 Terser 和 Closure Compiler。
在下面的测试中,每个构建工具都配置为通过其内置的“production”选项来优化 Bundle 包,或者在没有此类选项的情况下使用最常见的配置。 一些工具能够作为 Bundle 的一部分执行死代码消除,其他工具可能依赖于 Terser 等其他工具。
// index.js
import { logCaps } from './utils.js';
logCaps(exclaim('This is index'));
function thisIsNeverCalled() {
console.log(`No, really, it isn't`);
}
下面是 Utils.js 的内容:
export function logCaps(msg) {
console.log(msg.toUpperCase());
}
export function thisIsNeverCalledEither(msg) {
return msg + '!';
}
一旦为生产而构建, thisIsNeverCalled 和 thisIsNeverCalledEither 函数都应该从包中完全删除。
死导入代码(Dead Imported Code)
未由应用程序中的任何其他模块导入或使用的模块的导出也可以被视为死代码并需要删除。
然而,这可能会导致一些棘手的优化问题,因为模块的导出可能会以难以静态分析的方式被使用。 动态导入就是其中一种情况,因为动态导入返回的模块记录具有每个导出的属性,可以通过多种不同的方式引用这些属性,其中一些无法在构建时确定。
下面的测试用使用了两个模块 , 一个入口模块和一个动态导入以创建分割点的 utils.js 模块。动态导入的模块有两个导出,但只使用了 logCaps 导出。
// index.js
(async function () {
const { logCaps } = await import('./utils.js');
logCaps('This is index');
})();
下面是 utils.js 内容:
// utils.js
export function logCaps(msg) {
console.log(msg.toUpperCase());
}
export function thisIsNeverCalled(msg) {
return msg + '!';
}
一旦为生产而构建,utils.js 中的 thisIsNeverCalled 函数不应出现在生成的 Bundle 中。
然而,不同的打包工具在这个功能上表现差异非常大。如:browserify 不支持懒加载而无法实现该功能、rollup 不支持、parcel 表现亮眼、而 webpack 不支持如下的特殊解构语法。
const { logCaps } = await import('./utils.js');
但 webpack 允许手动列出通过魔术注释使用的导出:
const { logCaps } = await import(/* webpackExports: "logCaps" */ './utils.js');
不同框架的应对策略
React
如果不是服务器端渲染,可以使用 React.lazy() 拆分 JavaScript 包。否则,使用第三方库(例如:loadable-components)进行代码分割。
比如下面的示例,Loadable 允许开发者将动态导入渲染为常规组件:
import loadable from '@loadable/component';
const OtherComponent = loadable(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}
Vue
如果不是服务器端渲染并使用 Vue router,可以通过延迟加载路由拆分包。Vue Router 原生支持开箱即用的动态导入,这意味着可以用动态导入替换静态导入:
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [
{ path: '/users/:id', component: UserDetails }
// or use it directly in the route definition
{ path: '/users/:id', component: () => import('./views/UserDetails.vue') },
],
})
component(和components)选项接受一个返回组件 Promise 的函数,Vue Router 仅在第一次进入页面时获取,然后使用缓存的版本。 这意味着开发者还可以拥有更复杂的函数,只要它们返回 Promise:
const UserDetails = () =>
Promise.resolve({
/* component definition */
});
一般来说,最好始终对所有路由使用动态导入。当使用像 webpack 这样的打包器时将自动受益于代码分割 使用 Babel 时,需要添加 syntax-dynamic-import 插件,以便 Babel 能够正确解析语法。
转载作品,原作者:,文章来源:https://www.toutiao.com/article/7304467505488151040