前端性能精进(七)——构建

科技资讯 投稿 8000 0 评论

前端性能精进(七)——构建

这些任务包括语言编译、文件压缩、模块打包、图像优化、单元测试等一切需要对源码进行处理的工作。

构建工具从早期基于流的 gulp,再到静态模块打包器 webpack,然后到现在炙手可热的 Vite,一直在追求更极致的性能和体验。

一、减少尺寸

减少文件尺寸的方法除了使用算法压缩文件之外,还有其他优化方式也可以减小文件尺寸,例如优化编译、打包等。

1)编译

在编译完成后,JavaScript 或 CSS 文件的尺寸可能就会有所增加。

例如 ES6 的 Symbol 类型编译成 ES5 语法,如下所示,代码量激增。

let func = ( => { let value = Symbol(; return typeof value; }; // 经过 Babel 编译后的代码 function _typeof(obj { "@babel/helpers - typeof"; return ( (_typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj { return typeof obj; } : function (obj { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj ; } var func = function func( { var value = Symbol(; return _typeof(value; };

为了增加编译效率,需要将那些不需要编译的目录或文件排除在外。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx$/,
        use: "babel-loader",
        exclude: /node_modules/
      },
    ]
  }
};

2)打包

runtime 是一段辅助代码,在模块交互时,能连接它们所需的加载和解析逻辑,下面是通过 webpack 4.34 生成的 runtime。

/******/ (function(modules { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId] { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__; /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__ /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter { /******/ if(!__webpack_require__.o(exports, name { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }; /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }; /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }; /******/ }; /******/ /******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode { /******/ if(mode & 1 value = __webpack_require__(value; /******/ if(mode & 8 return value; /******/ if((mode & 4 && typeof value === 'object' && value && value.__esModule return value; /******/ var ns = Object.create(null; /******/ __webpack_require__.r(ns; /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }; /******/ if(mode & 2 && typeof value != 'string' for(var key in value __webpack_require__.d(ns, key, function(key { return value[key]; }.bind(null, key; /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module { /******/ var getter = module && module.__esModule ? /******/ function getDefault( { return module['default']; } : /******/ function getModuleExports( { return module; }; /******/ __webpack_require__.d(getter, 'a', getter; /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property { return Object.prototype.hasOwnProperty.call(object, property; }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0; /******/ } /************************************************************************/

在代码中定义了一个加载模块的函数:__webpack_require__(,其参数是模块标识符,还为它添加了多个私有属性。

也就是说,webpack 自己编写 polyfill 来实现 CommonJS、ESM 等模块语法。

所以,rollup 打包出的脚本比较干净(如下所示),适合打包各类库,React、Vue 等项目都是用 rollup 打包。

import { age } from './maths.js'; console.log(age + 1 console.log(1234 // maths.js 文件中的代码 export const name = 'strick' export const age = 30 // 经过 rollup 打包后的代码 const age = 30; console.log(age + 1; console.log(1234;

目前,支持 ES6 语法的浏览器已达到 98.35%,如下图所示,若不需要兼容 IE6~IE10 等古老浏览器的话,rollup 是打包首选。

3)压缩

例如压缩 HTML 的 html-minifier,压缩 JavaScript 的 uglify-js,压缩 CSS 的 cssnano,压缩图像的 imagemin。

webpack 和 rollup 都支持插件的扩展,在将上述压缩脚本封装到插件中后,就能在构建的过程中对文件进行自动压缩。

4)Tree Shaking

在执行 Tree Shaking 后,在文件中就不存在冗余的依赖和代码。在下面的示例中,ES 模块可以只导入所需的 func1( 函数。

export function func1( { console.log('strick' } export function func2( { console.log('freedom' } // maths.js 文件中的代码 import { func1 } from './maths.js'; func1(; // 经过 Tree Shaking 后的代码 function func1( { console.log('strick'; } func1(;

Tree Shaking 最先在 rollup 中出现,webpack 在 2.0 版本中也引入了此概念。

5)Scope Hoisting

下面是一个简单的示例,action( 函数直接被注入到引用它的模块中。

import action from './maths.js'; const value = action(; // 经过 Scope Hoisting 后的代码 (function( { var action = function( { }; var value = action(; };

注意,由于 Scope Hoisting 依赖静态分析,因此需要使用 ES6 模块语法。

比起常规的打包,在经过 Scope Hoisting 后,脚本尺寸将变得更小。

二、合并打包

1)Code Splitting

常用的优化手段是 Code Splitting,即代码分离,将代码拆成多块,分离到不同的文件中,这些文件既能按需加载,也能被浏览器缓存。

Vue 内置了一条命令,可以查看每个脚本的尺寸以及内部依赖包的尺寸。

在 vue.config.js 中,配置 config.optimization.splitChunks(,如下所示,参数含义可参考 SplitChunksPlugin 插件。

config.optimization.splitChunks( { cacheGroups: { vendors: { name: 'chunk-vendors', test: /[\\/]node_modules[\\/]/, priority: -10, chunks: 'initial' }, lottie: { name: 'chunk-lottie', test: /[\\/]node_modules[\\/]lottie-web[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true }, swiper: { name: 'chunk-swiper', test: /[\\/]node_modules[\\/]_swiper@3.4.2@swiper[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true }, lodash: { name: 'chunk-lodash', test: /[\\/]node_modules[\\/]lodash[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true } } }

在经过一顿初步操作后,原始尺寸降到 2.4M,gzipped 压缩后的尺寸是 308.64KB,比之前少了 100 多 KB,如下图所示。

例如常用的 lodash 或 underscore,都是些短小而实用的工具方法,只要单独提取并修改成相应的代码(参考此处),就能避免将整个库引入。

2)资源内联

像移动端屏幕适配脚本,就比较适合内联到 HTML 中,因为这类脚本要最先运行,以免影响后面样式的计算。

webpack 的 InlineSourcePlugin 就提供了 JavaScript 和 CSS 的内联功能。

还有一种内联是为资源增加破缓存的随机参数,以免读取到旧内容。

<script src="/js/chunk-vendors.e35b590f.js"></script>

在 webpack.config.js 中,有个 output 字段,用于配置输出的信息。

module.exports = {
  output: {
    filename: "[name].[hash].bundle.js"
  }
};

总结

在构建之前,也可以做一些前置优化。

还可以将一些库中的简单功能单独实现,以免引入整个库。这部分优化后,打包出来的尺寸肯定会比原先小。

在合并时,可以将那些第三方库提取到一起,组成一个单独的文件,这些文件既能按需加载,也能被浏览器缓存。

 

编程笔记 » 前端性能精进(七)——构建

赞同 (42) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽