欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Vite+rollup 项目如何显著提高性能

最编程 2024-04-25 19:25:06
...

前言:

本文主要介绍 Vite+rollup 项目是如何大幅提升性能、体验、优化

温馨提示:码字不易,先赞后看,养成习惯!!!

1:Vite

1.1 什么是 Vite?

引用官方的一句介绍:Vite 下一代前端构建工具链,为开发者提供极速响应

1.2 Vite开发环境为何这么快?

1:ESM

Vite是基于ESM这一块大基石来实现开发的极致体验,具体是什么我们可以简单回顾一下ESM (ES Modules)的历史。 2015年6月,TC39 发布了 ES6规范 也就是 ESM 规范,ESMJavaScript 官方的标准化模块系统,直至2017才得到了大多数主流浏览器的支持。在2018年5月Firefox 60 发布之后,所有的主流浏览器就都已开始广泛支持原生 ESM,这是 es当前提案 有兴趣可以去扫一眼。而 NodeJs8.9 之后的版本,就开始支持 ES6 了,但是在 13.2 版本之后才开启默认支持运行ES Modules

企业微信截图_1681798444109.png

我们可以从上图中看到 Vite 官网将其作为其最大的卖点之一,就是开发过程无需打包,直接加载 ESM 文件,开发者只需按照 ESM 规范提供 ESM 文件,然后交给浏览器处理

Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。

  • 依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。

    Vite 将会使用 esbuild 预构建依赖esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSXCSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。

    Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

企业微信截图_16818054982704.png

接下来我们看一段代码

企业微信截图_1681802472306.png

从上图中我们可以很清晰的看到这份文件引入了一个 main.js 的文件,并且这个文件是 ESM 类型文件

企业微信截图_16818027049269.png

上图我们看到引用的文件中都是我们数熟悉的 ESMimport 语法,这样写浏览器完全可以支持,(如果不支持怎么办?传统浏览器可以通过官方插件 @vitejs/plugin-legacy 支持)那么直接输出即可。所以 Vite 能做到快速启动,因为压根不需要打包最多也只是进行依赖预构建与源码转换,也就大大缩短了冷启动的时间。我们能感知到的结果就是秒启动,无论你的项目多大多复杂。相较于 webpack 而言呢,在我们的开发阶段我们需要启动我们本地环境的时候,webpack 会从入口开始收集所有的依赖项,构建出依赖图,然后将项目中所依赖的每一个模块逐一打包成 bundles。这个过程单较于启动会慢很多,特别是在构建大型应用的时候,第一次服务启动时间可能长达几分钟,这样大大降低了我们的开发效率。记得第一次使用 Vite 启动本地开发环境的时候,是震撼的!

2:HRM(Hot Module Replacement)

在使用开发环境开发的时候也有一项对开发者的感知特别明显,就是 HMR (Hot Module Replacement)。其做到了无感知更新,而相较于 webpack 而言每一次都需要等待几秒,还会刷新页面。对于 ViteHMR 却不存在这些问题。我们可以打开 Vite 开发的项目,在控制台就可以看到

企业微信截图_16818063785311.png

好家伙,这个为啥会出现在 head 的第一行?,我们写的源码中是没有这一句,后面了解后才明白这个是 Vite 做了一段注入,我们再去看看他都干了什么?

企业微信截图_16818066324829.png

运行这份文件可以在浏览器与本地服务器之间建立一个 websoket 连接,HMR 这个过程就是通过这个进行。大致梳理一下,流程如下

企业微信截图_16818075237353.png

上图大致的工作逻辑:收集变动->通知变动->浏览器执行变动

一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活,但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热替换)(HMR): 允许一个模块 “热替换” 它自己,而不会影响页面其余部分。这大大改进了开发体验 —— 然而,在实践中我们发现,即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。

Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

以上介绍了 Vite 为何能做到开发环境迅速准备完毕 + 高效HMR大致原理

总结

Vite 工具的使用能够让开发者节约没有必要的等待时间提升开发效率,同时拥有更好的开发体验,使开发连贯性更好。实际启动时间如下:

企业微信截图_16818172283906.png

2:rollup

2.1:什么是 rollup

JavaScript 打包器

Vite 为什么选用了 rollup 作为打包器,而不是 webpack

  1. rollup 本身的轻量定位有关
  2. 有着良好的 ESM 模块化支持

如下:

企业微信截图_16818107657838.png

上图可以很明显看出实际上 rollup 的包大小只有 webpack 的1/4!!!

企业微信截图_16818113019627.png

上图 rollup 将主要特性之一就是友好支持 ESM

企业微信截图_1681811580647.png

上图可以看出 webpack 是支持 ESM

目前打包器能力越来越趋同,没有很明显差异。所以以上的理由感觉都比较牵强,其实基本上的功能双方都大差不差,更多使用取向来源于习惯与熟悉程度。

这里看一下尤大在知乎的回答,也许能找到一些缘由,其中有一段说到了 webpackrollup

3:重点来了:我做了什么?

3.1:先看结果:数据告诉你(基于Google lighthouse 得出的前后对比数据)

1:首屏优化前: 企业微信截图_16705812209741.png优化后 企业微信截图_16818190598793.png

2:Home页优化前: 企业微信截图_16705812929204.png优化后 企业微信截图_1681818739928.png

3:Device页优化前: 企业微信截图_16705813561212.png优化后 企业微信截图_16818187712129.png

4:Energy页优化前: 企业微信截图_16705815123605.png优化后 企业微信截图_16818189895505.png

上图的一组数据,这组数据展示的是优化前后 GoogleLightHouse 给出的站点分值,可以看出未做优化之前站点的整体分数偏低,性能指标基本不合格。优化完成后达到期望,使用下来整个站点的反馈速度也有明显提升,总体效果符合期望。

4:怎么做?

前置条件:站点使用的协议为 h1.1

根据 h1.1 的一些优缺点定制部分优化逻辑。但无论是 h1.1 还是 h2 优化大部分是通用的

大方向上有以下优化点:

  1. 压缩
  2. 文件hash(缓存)
  3. 聚合碎片
  4. 环境区分

4.1:压缩

对于 To C 网站大部分存在大量的图片,我粗略的统计了一下图片所耗费的流量已经超过了整个站点的 60% 以上的流量,所以对图片的优化将至关重要也是个老生常谈的问题。

4.1.1:图片格式选择

我们站点选择的是webp,是一种全新一代的图片格式。由谷歌2010年推出。在《web前端性能优化》这本书中也有提及。该格式图片拥有当前市面上绝大多数图片的优点集一身,实际使用下来在同等视觉体验下可以将图片所占的内存空间减小20%-50%,是一个非常优秀的图片格式,假设即使我们不对图片进行压缩也能在获得更小的图片输出,极大节约我们的带宽,提升加载速度。

企业微信截图_16818214812359.png

看一下 caniuse 在当前使用 webp 图片应该没有后顾之忧了,放心大胆用(IE已死!)。

4.1.2:图片压缩

对于图片压缩市面上有很多打包器插件都可以做,具体配置也比较简单想做统一压缩的可以选择这个 rollup的配置插件:rollup-plugin-imagemin,这个插件的配置以及如何使用不是本文的重点,有兴趣的可以去试用一下。

如果想单独压缩某一些大文件的推荐使用这个 图片压缩。个人觉得非常好用,压缩效果好(100%质量的情况下基本可以做到和原图无异,还能很大程度压缩大小),支持批量导入,批量下载,把需要压缩的图片批量导入选择压缩参数即可完成压缩。

4.1.3:svg压缩

一些使用比较简单的,重复使用的小图标可以用 svg 格式,svg 的好处不用我说了具体可以 看这一篇,有详细说明。但使用这个也是要注意的,就是对于复杂图标还是不建议使用 svg,相对而言其大小会变得非常巨大,得不偿失。还有一点就是别用多了,适量最好(个人经验是50个以内),多了会影响你的首页展示速度!具体可以观察以下这个文件的大小。没有优化之前这个文件将近 500k,现在只有大概 68k。首屏文件在网路上传输的时间大幅缩减。原来(300-500)ms --> 现在(50-150)ms

企业微信截图_16819718323940.png

如果有可能尽量控制在 14.4k 以内,能会进一步提升 FCP (first content painting)

对于 svg 压缩,我选择的是 svgo 用过都说好!

1:首先下载改安装pnpm -g install svgo

2:准备一个文件夹来承接压缩后的 svg 文件

企业微信截图_16818732368387.png

3:配置 svgo.config.js

module.exports = {
  plugins: [
    'removeDoctype',
    'removeXMLProcInst',
    'removeComments',
    'removeMetadata',
    'removeEditorsNSData',
    'cleanupAttrs',
    'inlineStyles',
    'minifyStyles',
    // 'cleanupIDs',
    'removeUselessDefs',
    'cleanupNumericValues',
    'convertColors',
    'removeUnknownsAndDefaults',
    'removeNonInheritableGroupAttrs',
    'removeUselessStrokeAndFill',
    // 'removeViewBox',
    'cleanupEnableBackground',
    'removeHiddenElems',
    'removeEmptyText',
    'convertShapeToPath',
    'convertEllipseToCircle',
    'moveElemsAttrsToGroup',
    'moveGroupAttrsToElems',
    'collapseGroups',
    'convertPathData',
    'convertTransform',
    'removeEmptyAttrs',
    'removeEmptyContainers',
    'mergePaths',
    'removeUnusedNS',
    'sortDefsChildren',
    'removeTitle',
    'removeDesc'
  ]
}

配置项可以参考一下,具体可以去 GitHub 看一下每一项含义再做定制化的压缩,这里不多介绍

4:在 package.json 文件里设置命令行做配置即可

"svgo": "svgo -f <你的源文件地址> -o <输出的压缩文件地址> --config svgo.config.js"

5:将 svgo 作为配置命令嵌入到你项目的整个构建流程中即可

"build": "pnpm svgo && vite build"

使用起来非常方便,不受架构限制,简单配置过就可以跑起来了

企业微信截图_16818737689049.png

4.1.4:小图片处理

由于站点中还有大量的 1-10kb小图片,但是不适用于 svg 那么我们可以通过配置来将其转成 base64url,图片被转换成类似于这样的一串字符串data:image/webp;base64,UklGRhALAABX...O/AA/fPxcP278tD9s/RA/Lv1pPoP9kAgA= 浏览器就不用再去下载,可以极大的减少 http 请求。但也有一些问题,转成 Base64 之后文件大约会增大 1/3,本质上网络还是要承担这一部分流量,具体是由于 Base64 要求把每三个8Bit的字节转换为四个 6Bit 的字节 (3*8 = 4*6 = 24),然后把 6Bit 再添两位高位0,组成四个 8Bit 的字节,也就是说,转换后的字符串理论上将要比原来的长1/3

可以通过以下的配置将小图片转 base64

企业微信截图_16818742694666.png 项目中通过配置,建议配置10k以下的值,不配置默认4kb 直达链接

转换规则

关于这个编码的规则:

①把3个字节变成4个字节

②每76个字符加一个换行符

③最后的结束符也要处理

RFC 4648 标准的 Base64 索引表 企业微信截图_16818759541593.png

例子
  • 首先,将二进制数据中每三组 8 个二进制位”重新分组为四组 6 个二进制位
  • 然后,每组的 6 个二进制位用一个十进制数来表示。6 个二进制位可表示的十进制数的范围是 0 - 63
  • 接下来,根据 Base64 索引表,将每组的十进制数转换成对应的字符,即每组可以用一个可打印字符来表示

ManBase64 编码结果为 TWFu,详细原理如下: 企业微信截图_16818762026838.png 代码实现

if (!Shotgun)
    var Shotgun = {};
    if (!Shotgun.Js)
    Shotgun.Js = {};
    Shotgun.Js.Base64 = {
        _table: [
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
            'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
            'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
        ],
        encode: function (bin) {
            var codes = [];
            var un = 0;
            un = bin.length % 3;
            if (un == 1)
                bin.push(0, 0);
            else if (un == 2)
                bin.push(0);
            for (var i = 2; i < bin.length; i += 3) {
                var c = bin[i - 2] << 16;
                c |= bin[i - 1] << 8;
                c |= bin[i];
                codes.push(this._table[c >> 18 & 0x3f]);
                codes.push(this._table[c >> 12 & 0x3f]);
                codes.push(this._table[c >> 6 & 0x3f]);
                codes.push(this._table[c & 0x3f]);
            }
            if (un >= 1) {
                codes[codes.length - 1] = "=";
                bin.pop();
            }
            if (un == 1) {
                codes[codes.length - 2] = "=";
                bin.pop();
            }
            return codes.join("");
        },
        decode: function (base64Str) {
            var i = 0;
            var bin = [];
            var x = 0,
                code = 0,
                eq = 0;
            while (i < base64Str.length) {
                var c = base64Str.charAt(i++);
                var idx = this._table.indexOf(c);
                if (idx == -1) {
                    switch (c) {
                        case '=':
                            idx = 0;
                            eq++;
                            break;
                        case ' ':
                        case '\n':
                        case "\r":
                        case '\t':
                            continue;
                        default:
                            throw {
                                "message": "\u0062\u0061\u0073\u0065\u0036\u0034\u002E\u0074\u0068\u0065\u002D\u0078\u002E\u0063\u006E\u0020\u0045\u0072\u0072\u006F\u0072\u003A\u65E0\u6548\u7F16\u7801\uFF1A" +
                                    c
                            };
                    }
                }
                if (eq > 0 && idx != 0)
                    throw {
                        "message": "\u0062\u0061\u0073\u0065\u0036\u0034\u002E\u0074\u0068\u0065\u002D\u0078\u002E\u0063\u006E\u0020\u0045\u0072\u0072\u006F\u0072\u003A\u7F16\u7801\u683C\u5F0F\u9519\u8BEF\uFF01"
                    };

                code = code << 6 | idx;
                if (++x != 4)
                    continue;
                bin.push(code >> 16);
                bin.push(code >> 8 & 0xff);
                bin.push(code & 0xff)
                code = x = 0;
            }
            if (code != 0)
                throw {
                    "message": "\u0062\u0061\u0073\u0065\u0036\u0034\u002E\u0074\u0068\u0065\u002D\u0078\u002E\u0063\u006E\u0020\u0045\u0072\u0072\u006F\u0072\u003A\u7F16\u7801\u6570\u636E\u957F\u5EA6\u9519\u8BEF"
                };
            if (eq == 1)
                bin.pop();
            else if (eq == 2) {
                bin.pop();
                bin.pop();
            } else if (eq > 2)
                throw {
                    "message": "\u0062\u0061\u0073\u0065\u0036\u0034\u002E\u0074\u0068\u0065\u002D\u0078\u002E\u0063\u006E\u0020\u0045\u0072\u0072\u006F\u0072\u003A\u7F16\u7801\u683C\u5F0F\u9519\u8BEF\uFF01"
                };

            return bin;
        }
    };
  • 优点:节约 http 请求
  • 缺点:项目文件稍许变大

1.5:文件gzip

1:安装:pnpm i -g vite-plugin-compression

2:引入:import viteCompression from 'vite-plugin-compression'

3:使用:

viteCompression({
  threshold: 1024,
  filter: /\.(css|html|js|svg|json)$/i,
  deleteOriginFile: false,
  algorithm: 'gzip'
})

配置好了后端支持一下就生效了,对比了一下整体 压缩了60% 左右,看一下效果

企业微信截图_16819748922647.png

1.6:其他

企业微信截图_16818766207809.pngterser 具体根据自己需求来定,配置项请 移步这里

2:文件hash(缓存)

企业微信截图_16818849061119.png

企业微信截图_16818850378284.png

企业微信截图_16818850842704.png

企业微信截图_16818851253878.png

chunkFileNames: 'static/js/[name]-[hash].js', // 引入文件名的名称
entryFileNames: 'static/js/[name]-[hash].js', // 包的入口文件名称
assetFileNames: 'static/[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等

看一下效果

企业微信截图_16818856579119.png

3:聚合碎片

聚合碎片的本意是要减少小文件的 http 请求,把这些小文件聚合到该页面需要请求的文件之中。想要的结果是一个http 请求可以涵盖几十个碎片请求(由于浏览器限制一个域下最多允许 6个tcp 同时存在),这样对于整个项目来说其实是非常有利的,把小文件直接并入大文件中,只需要拉取少数的几个文件就可以。减少了 tcp 连接次数,避免的大量的慢启动。同时也可以尽量的减少等待时间。所以综上站点策略将去聚合大量碎片,并尽量保持大文件个数控制在 6 个。

企业微信截图_16818864733322.png 通过 rollup 提供的 api,我们能抓住每一个碎片,将其按照每个页面进行高度的定制化。可以通过 id 这个参数进行正则匹配,进行分包或者聚合都行。具体里面的逻辑怎么写我就不贴了,毕竟每一个项目不一样不具有通用性,但是思路和方式相似,按照自己的想法去写即可。

4:环境区分

环境区分这个思路比较纯粹,就是针对不同的环境做不同的配置策略 比如:开发环境我希望能输出 log,那么就会在打包编译的时候去判断到底现在打包是哪一个环境,如果是开发,测试那么就会保留一些 log、debugger、err、sourcemap、comment 等,如果是生产环境就会屏蔽这些。

配置如下代码

sourcemap: !isPro,
minify: 'terser',
terserOptions: {
    compress: {
      drop_console: isPro, // 删除console
      drop_debugger: isPro // 删除 debugger
    },
    format: {
      comments: false // 去掉注释内容
    }
}

配置过你会发现,每个环境的包大小差距很大,生产环境的包可能只有开发环境的一半大小。

5:除了这些还能做什么优化?

  • 预解析域名
  • 优先下载样式表
  • 文件预下载
  • 异步加载执行
  • webworker
  • CDN(Content Delivery Network)
  • 图片渲染为原始尺寸
  • 聚合/离散css
  • 关键渲染路径优化
  • 适当增加图层
  • 消除意外引用变量
  • 闭包
  • ...

优化项目很多暂时能想到这些,但每一个项目不一样比如你的项目是h2而我的项目是 h1.1,优化策略上会有些许取向不同。

5:总结

以上就是关于Vite + rollup打包部署优化实践的全部内容,目的也很明确 提效+优化,对于结果来说自己是满意的,但是也有几点遗憾

  • 首屏的 document 文件没有支持 gzip
  • 暂时无法支持 h2

有时候不管是工具的选择还是优化的策略都依赖于一个更高维度视角,路遥!

6:推荐好文

  • 前端模块化CommonJS/AMD/CMD/ESM(完整版)
  • 最佳实践 monorepo + pnpm + vue3 + element-plus 0-1 完整教程
  • Vite+rollup项目如何大幅提升性能体验
  • 面试官系列:请说说你对深拷贝、浅拷贝的理解
  • 面试官系列:请你说说原型、原型链相关
  • 面试官系列:请手写防抖或节流函数debounce/throttle
  • 面试官系类:请手写instanceof
  • 10分钟快速手写实现:call/apply
  • 5分钟快速手写实现:bind
  • 5分钟快速手写实现:new
  • 10分钟入门SVG:SVG如何画出单身狗?来本文告诉你

7:参考

  • cn.vitejs.dev/
  • cn.rollupjs.org/
  • Make the Web Faster
  • terser.org/docs/api-re…
  • JavaScript 二十年
  • ES6 入门教程
  • kangax.github.io/compat-tabl…
  • github.com/malchata/ro…
  • blog.****.net/weixin_4983…
  • Web Performance Tips ⚡
  • www.zhihu.com/question/47…
  • baike.baidu.com/item/base64…
  • 262.ecma-international.org/6.0/
  • tc39.es/ecma262/
  • developer.chrome.com/docs/devtoo…
  • JavaScript与二进制数据的恩怨情仇
  • github.com/alloc/vite-…
  • HTTP/2 相比 1.0 有哪些重大改进?
  • How to Build Large Sites effectively on Netlify