前端性能优化 - 代码拆分和按需加载(减少bundle size)

代码拆分和按需加载的设计决定着工程化构建的结果,将直接影响应用的性能表现,因为合理的加载时机和代码拆封能够使初始代码体积更小,页面加载更快。

按需加载和按需打包区别

按需加载表示代码模块在交互需要时,动态引入;而按需打包针对第三方依赖库,及业务模块,只打包真正在运行时可能会需要的代码。

按需打包的方式

  • 使用ES Module支持的Tree Shaking方案,在使用构建工具打包时,完成按需打包
  • 使用以babel-plugin-import为主的Babel插件,实现自动按需打包
Tree Shaking实现按需打包

假设业务中使用了antd的Button组件:

import { Button } from 'antd';

这样的引用,会使得最终打包文件中包含所有antd导出来的内容。假设应用中并没有使用antd提供的TimePicker组件,那么对于打包结果来说,无疑增加了代码体积。在这种情况下,如果组件库提供了ES Module版本,并开启了Tree Shaking,我们就可以通过摇树特性,将不会使用到的代码在构建阶段移除。

webpack可以在package.json文件中设置sideEffects: false。在antd源码中可以找到:

"sideEffects": [
	"dist/*",
	"es/**/style/*",
	"lib/**/style/*",
	"*.less"
]

来指定副作用模块。

学习编写Babel插件,实现按需打包

如果第三方库不支持Tree Shaking,我们依然可以通过Babel插件,改变业务代码中对模块的引用路径来实现按需打包。
babel-plugin-import这个插件。我们通过一个例子来了解他的原理。

import { Button as Btn, Input, TimePicker,  ConfigProvider, ...} from 'antd'

这样的代码就可以被编译为:

import _ConfigProvider from 'antd/lib/config-provider';
import _Button from 'antd/lib/buttonl';
import _Input from 'antd/lib/input';
import _TimePicker from "antd/lib/time-picker";

Babel插件核心依赖于对AST的解析和操作。它本质上就是一个函数,在Babel对AST语法书进行转换的过程中介入,通过相应的操作,最终让生成结果发生改变。
Babel已经内置了几个核心分析、操作AST的工具集, Babel插件通过观察者 + 访问者模式,对AST节点统一遍历,因此具备了良好的扩展性和灵活性。

动态引入(dynamic Import)

静态导入:
标准用法的import属于静态导入,它只支持一个字符串类型的module specifier(模块路径声明),这样的特性会使所有被import的模块在加载时就被编译。

这种做法从某种角度上看,对于绝大多数场景来说性能是友好的,因为意味着对工程代码静态分析称为了可能,进而使得类似tree-shaking的技术有了应用空间。

但对于一些特殊场景,静态导入也可能称为性能的短板,如,当我们需要:

  • 按需加载一个模块
  • 按运行时间选定一个模块

dynamic import。如在浏览器侧,根据用户系统语言选择加载不同的语言模块,根据用户的操作去加载不同的内容逻辑。如根据用户系统语言选择加载不同的语言模块。

MDN文档中给出了dynamic import 更加具体的使用场景:

  • 静态导入的模块很明显降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它;
  • 静态导入的模块很明显占用了大量的系统内存,且被使用的可能性很低;
  • 被导入的模块在加载时并不存在,需要异步获取;
  • 导入模块的说明符,需要动态构建(静态导入只能使用静态说明符);
  • 被导入的模块由副作用(可以理解为模块中会直接运行的代码),这些副作用的代码只有在触发某些条件时才被需要。

dynamic import的标准用法文档:官方规范tc39 proposal

// html 部分
<nav>
  <a href="" data-script-path="books">Books</a>
  <a href="" data-script-path="movies">Movies</a>
  <a href="" data-script-path="video-games">Video Games</a>
</nav>
<div id="content"></div>

// script 部分
<script>
  // 获取 element
  const contentEle = document.querySelector('#content');
  const links = document.querySelectorAll('nav > a');
  
  // 遍历绑定点击逻辑
  for (const link of links) {
    link.addEventListener('click', async (event) => {
      event.preventDefault();
      try {
        const asyncScript = await import(`/${link.dataset.scriptPath}.js`);
        // 异步加载脚本
        asyncScript.loadContentTo(contentEle);
      } catch (error) {
        contentEle.textContent = `We got error: ${error.message}`;
      }
    });
  }
</script>

当点击页面上的a标签,会动态加载一个模块,并调用模块定义的loadContentTo方法完成页面内容的填充。

表面上看,await import()的用法使得import像一个函数, 该函数通过调用()操作符调用并返回一个Promise。
事实上,dynamic import只是一个function like的语法形式。在ES class特性中,super()与dynamic import类似,也是一个function like语法形式。所以它和函数有着本质的区别。

  • dynamic import并非继承自Function.prototype,因此不能使用Function构造函数原型上的方法import.call(null, ${path}), 调用它是不合法的。
  • dynamic import并非继承自Object.prototype,因此不能使用Object构造函数原型上的方法。

虽然dynamic import并不是一个真正意义上的函数,但我们可以通过实现一个dynamicImport函数来模拟实现dynamic import,并进一步加深对其语法特性的理解。

实现一个dynamic import(动态引入)

const importModule = (url) => {
  return new Promise((resolve, reject) => {
    // 创建一个script标签
    const script = document.createElement('script');
    const tmpGlobal = `__tempModuleLoadingVariable${Math.random().toString(32).substr(2)}`;

    script.type = 'module';
    script.textContent = `import * as m from "${url}"; window.${tmpGlobal}=m`;

    // load回调
    script.onload = () => {
      resolve(window[tmpGlobal]);
      delete window[tmpGlobal];
      script.remove();
    };

    document.documentElement.appendChild(script);
  });
};

在以上函数中,通过动态插入一个script标签实现对目标script url的加载,将模块到处内容赋值给window对象。

webpack赋能代码拆分和按需加载

webpack提供了三种相关能力:

  • 通过入口配置手动分隔代码
  • 动态导入支持
  • 通过splitChunk插件提取公共代码(公共代码分隔)

webpack对dynamic import 能力支持

webpack中splitChunk插件和代码分隔
代码分割和动态加载是两个不同的概念。 动态加载技术本质上是一种懒加载 ---- 按需加载,即只有在需要的时候才会加载代码。而代码分割是一种代码拆包技术,与代码合并打包是一个相逆的过程。

代码分割的核心意义在于避免重复打包以及提升缓存利用率,进而提升访问速度。如将不常变化的第三方依赖进行代码拆分,方便对第三方依赖库缓存,同时抽离公共逻辑,减少单个文件的size大小。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值