ES 模块与浏览器特性:前端优化的新思路

前言

现在的很多前端开发者,不管是出于代码可读性、代码简洁性还是其他可能存在炫技成分的原因,在编写 JS 代码时都喜欢用上最新的 ECMAScript 语法。但大多数的项目都还是会选择通过 babel 将其转换成 ES5 语法,并加上一些必要的 pollfill 去兼容小部分用户使用的不支持 ES2015 的浏览器。

但在理想情况下, 我们其实不需要在浏览器上运行一些不必要的代码,大部分用户使用的浏览器已经支持了 ES205+ 的语法了。

借助新的 JavaScript 和 DOM API,我们可以对新语法进行特征检测。而且,我们现在也确实有一种对基本 ES2015 语法支持进行特征检测的方法。

type module

随着 es6 的发布,各大浏览器也于2018年5月开始支持 <script type="module">。至今为止,全球已有96.7%的用户使用的浏览器都支持该功能。

<script type="module"> 是加载 ES 模块的方法,但 ES 模块的发布同时也伴随着ES2015+ 功能的常规语法,如 async/await、classes、箭头函数、fetch、Promises、Map、Set等。因此,该功能可用于加载ES2015+ 的JavaScript 文件并知道浏览器可以处理。

为了兼容小部分用户使用的浏览器,该功能基本上会与 nomodule 同时存在。

html 复制代码
<!-- Browsers with ES module support load this file. -->
<script type="module" src="main.modern.js"></script>

<!-- Older browsers load this file (and module-supporting -->
<!-- browsers know *not* to load this file). -->
<script nomodule src="main.legacy.js"></script>

如果我们现在的项目已经支持了 es5 版本的编译,那么我们现在所需要做的就是生成 ES2015+ 版本。

编译实现

如果我们当前已经在使用 webpack、rollup、gulp 等编译器将项目编译成 es5 的 bundle 脚本,那么我们就还需要使用这些编译器,生成一个 ES2015+ 的 bundle 脚本,同时需要将 polyfill 处理成按需加载。

如果我们的工程中使用了 babel-preset-env,那么生成一个 ES2015+ 就更加简单了。我们所需要的就是在入口html 文件中加载 bundle 文件的地方修改成<script type="module"><script nomodule> 的形式就可以了。

改造一下项目的 webpack,使其在编译时编译两套 bundle 脚本,假设入口文件为 './path/to/main.mjs'

javascript 复制代码
function baseConfig(type) {
  const isModern = type === 'es2015'
  const output = isModern ? 'main.modern.js' : 'main.legacy.js'
  // babel的配置有两种方式处理,可以使用esmodules 配置,
  // 也可以将es2015+支持的浏览器最低版本号进行列举到browsers中
  // esmodules 方式处理
  const targets = isModern
    ? { esmodules: true }
    : { browsers: ['ios >= 10', 'android >= 5.1'] }
  // browsers 方式处理
  const targets = isModern
    ? { browsers: ['Chrome >= 60',
                  'Safari >= 10.1',
                  'iOS >= 10.3',
                  'Firefox >= 54',
                  'Edge >= 15',] }
    : { browsers: ['ios >= 10', 'android >= 5.1'] }

  return {
    entry: './path/to/main.mjs',
    output: {
      filename: output,
    },
    module: {
      rules: [{
        test: /.m?js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['env', {
                modules: false,
                useBuiltIns: true,
                targets: targets
              }],
            ],
          },
        },
      }],
    },
  }
}

export default baseConfig

在处理完 webpack 的配置后,接下来我们再进行两次编译。这样做就可以基于同一个 entry 脚本生成一套 ES5 版本的 bundle 和一套 ES2015+ 版本的 bundle。这样一来,我们既能够完美兼容低版本浏览器,又能够利用 ES2015+ 新语法带来的优势了。

javascript 复制代码
const fs = require('fs-extra')
const babelSupport = require('./babel-support')
const merge = require('webpack-merge')
const baseConfig = require('./config-base.js')
const proConfig = require('./config-pro.js')
const devConfig = require('./config-dev.js')


handle()

async function handle() {
  // 构建前清空下构建的目标目录
  await fs.remove('build')
  const isPro = process.NODE_ENV === 'production'
  
  await build(merge(baseConfig('es2015'), isPro ? proConfig() : devConfig()))

  await build(merge(baseConfig('legacy'), isPro ? proConfig() : devConfig()))

}

async function build(webpackConfig) {
  const compiler = webpack(webpackConfig)
  return new Promise((resolve, reject) => {
    compiler.run((err, status) => {
      if (err) {
        reject()
        throw err
      }
      resolve()
    })
  })
}

通过以上的配置编译,我们会得到两个可用于生产的 JS 文件

  • main.modern.js(语法为 ES2015+)
  • main.legacy.js(语法为 ES5)

然后更新 HTML 文件,以便在支持 ES 模块的浏览器中有条件地加载 ES2015+ 包。可以通过判断当前浏览器的版本号来加载不同的 JS 文件,也可以通过<script type="module"><script nomodule>的组合来完成此操作 :

方法1:将下面的 JS 脚本放在 html 文件的 <body> 前进行执行

javascript 复制代码
// 版本分界线
var browser = {
  firefox: '54',
  safari: '10.1',
  iOS: '10.3',
  chrome: '60',
  edge: '15',
}


var s = document.createElement('script');
if (p && v && (compareVersion(v, browser[p]) > 0 || compareVersion(v, browser[p]) === 0)) {
  // 使用高版本
  s.src = "./main.modern.js"
} else {
  // 使用低版本
  s.src = "./main.legacy.js.js"
}
document.head.appendChild(s);

方法2:将下面的代码放在 html 文件中

html 复制代码
<script type="module" src="main.modern.js"></script>

<script nomodule src="main.legacy.js"></script>

polyfill按需加载

由于上述的方式对于双版本的分界线比较清晰,可以根据该版本的分界线将 polyfill 拆分成 es5 版本和 es2015 版本,在 babel 编译时根据编译的配置控制 entry 中是否需要添加 es5 版本的 polyfill。

语法检测

除了根据 ES 模块支持情况获取的固定的版本分界线外,还可以通过检测项目中使用的语法来确定浏览器版本的分界线。

目前市面上有两种可用的语法检测工具:es-check 和 mpx-es-check。但对于我们的场景,这两个工具都不完全符合要求。

es-check(由社区提供)只能检测源码中是否存在不符合对应 ECMAScript 版本的语法,并且只能提示第一个不符合项。

mpx-ex-check(滴滴出品)能够检测 bundle 中所有的不符合项,并将它们输出到相应的文件中。

针对我们的场景,我们需要根据检测到的语法及其对应的浏览器支持版本,获取到当前项目在不使用 babel-preset-env 编译时所支持的各大浏览器的最低版本号,然后根据该版本号配置编译的 babel 配置。目前上述两个工具都无法支持这一需求,我们可以基于 mpx-ex-check 进行增强,实现这样的功能。

babel 配置

javascript 复制代码
{
    ...
    module: {
      rules: [{
        test: /.m?js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: isModern ? undifined : [
              ['env', {
                targets: { browsers: ['ios >= 10', 'android >= 5.1'] }
              }],
            ],
          },
        },
      }],
    },
  }

通过检测以上编译脚本构建出来的 modern 版本的 bundle,就可以获取到当前 modern 版本的 bundle 最低支持的各大浏览器版本号。比如我们的框架模版库检测出来的 modern 版本最低支持的各大浏览器版本号分别是:

  • Chorme > 80
  • Firefox > 74
  • safari > 13.1
  • iOS > 13.1

然后进行 bundle 的按需加载(根据浏览器版本号)处理。此时 modern 版本将不再需要加载 polyfill。

价值

理论上因为 modern 版本减少了大量的新语法转换,构建后的 bundle 脚本体积将会降低,此时,运行时加载 js 的耗时也会因此降低。

以下 modern bundle 数据来自于以语法检测出的 bundle 最低支持浏览器版本号作为版本分界线的方案。

体积优化

体积的降低也能让我们的支持运行 modern 版本的用户群体受益。以我们框架模版库的脚本为例

拆分多版本后体积大小如下,新语法版本能节约大概 22% 的体积

版本 dev 版本 pro 版本
main.modern.js 1.5M 539k
main.legacy.js 2M 680k

加载性能优化

bundle 脚本在 chrome 浏览器上单独运行的加载耗时

pro版本 加载耗时均值
main.modern.js 641ms
main.legacy.js 864ms

我们框架模版库的代码量本身也足够重,不过本身我们的脚本在终端本地,所以加载上的耗时对于我们来说没有太大的影响,以上数据来自于我们框架运行在 chrome 120 模拟器上的数据,作为数据参考。

运行性能优化

接下来就是脚本的运行了。Six-Speed 平台已经整理了 ES6 相关语法在进行 Babel 编译和不进行编译时在各个平台上的运行性能数据。详情可以查看 incaseofstairs.com/six-speed 。那么针对我们上述对我们框架脚本做的处理,分别统计一下 modern bundle 和 legacy bundle 的初始化模块在各个版本浏览器上的运行 5 次的耗时数据均值

pro版本 chrome 120 iOS 16.0 iOS 17.2 安卓 chromium 99
main.modern.js 1248ms 1252ms 1149ms 1319ms
main.legacy.js 1744ms 1545ms 1443ms 2589ms
-28% -18% -20% -49%

根据以上的数据,可以看出我们的框架模版库脚本 modern 版本的初始化模块在各个浏览器上运行都有一定的性能优化,在安卓上的表现最明显。当然,这数据也比较依赖于初始化模块中使用到的新语法数据。

总结

随着 < script type="module"> 标签的出现以及现代浏览器对 ES2015+ 语法的广泛支持,将ES2015+语法应用于生产环境已成为前端开发的趋势。在生产环境采用ES2015+语法的价值,不仅在于优化代码体积、加载性能、运行性能,更在于跟随时代潮流,为我们的网站带来更好的体验和效果,使我们网站的广大用户群体受益。

相关推荐
汪子熙17 分钟前
Angular 服务器端应用 ng-state tag 的作用介绍
前端·javascript·angular.js
Envyᥫᩣ26 分钟前
《ASP.NET Web Forms 实现视频点赞功能的完整示例》
前端·asp.net·音视频·视频点赞
Мартин.4 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。6 小时前
案例-表白墙简单实现
前端·javascript·css
数云界6 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd6 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常6 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer6 小时前
Vite:为什么选 Vite
前端
小御姐@stella6 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing6 小时前
【React】增量传输与渲染
前端·javascript·面试