🚀对比 webpack ,vite 开发为什么这么丝滑?

117 阅读7分钟

前言: 看到站内很多关于 “vite为什么这么快?” 这个问题,自己也是产生了浓厚的兴趣,于是有了本篇文章的动机。

First of all,这个问题其实是一个伪命题。“”在这里是一个形容词,我们其实想问的是它为什么开发体验很快,但其实它是需要参照物来进行比较,然后才能得出谁快谁慢的。就好像我们在学校学习的时候,“李雷的英语成绩很好”,实际上李雷的成绩只有30分,说出这句话的人是只考了5分的王梅梅。所以本篇文章会以 webpack 作为对照组,来分析一下为什么使用 vite 开发体验这么丝滑。

一. 准备工作

  1. 我们为了对比,需要准备用 vite 搭建的工程,和 webpack 搭建的工程(以 vue 为例)。因为 @vue/cli 的 dev-server 内部封装的就是 webpack,故我们直接采用 @vue/cli 来创建 webpack 作为对照组。 image.png

  2. 分别运行以下命令

    pnpm create vite vite-test --template vue

    npx @vue/cli create webpack-test

  3. 这是我们目前的目录结构。
    image.png

二. 排除误区

  1. 首先我们需要理清楚概念,我们这里说的 vite 比 webpack 快,绝大部分场景是特指在开发环境下,对于开发人员的开发体验来讲,vite 要比 webpack 体验好很多。并不是指代码运行速度快,代码运行效率这个问题并不是构建工具关心的,两者打包后的产物 js 文件,在生产环境下的运行速度,如果两者没有进行十分特殊的配置,其实相差无几。

    ps: 打包后不同的文件大小,影响的也只是网络传输速度,和代码真正的运行效率无关。

  2. 我们分别在各自的目录下运行 pnpm run build 命令,然后从 dist 目录下可以看到各自的打包结果,从打包结构来看是没有什么区别的。你可能会注意到 webpack 的 js 打包结果里有两个文件。其中一个叫 chunk-vendor。这里包含了前端优化的一个点,就是浏览器缓存问题。我们先来讲解一下为什么要这样做?
    image.png image.png

  3. 首先我们要搞清楚的是,这个 dist 目录就是我们要上传到服务器的最终代码,当用户通过浏览器访问我们的界面时,会请求 index.html 的内容,当浏览器解析到 script 标签时,会再次发起一个请求来获取对应的 js 文件。 image.png

  4. 而我们的前端项目结构往往是这样的,一部分 js 是我们自己写的代码,另一部分是引用的一些第三方库。随着我们的界面日益完善,功能越来越多,我们自己书写的这一部分代码是会很频繁的更新迭代的,而这些第三方库是基本上不变的。如果我们把所有的 js 代码都放在同一个文件里,即使每次改动很小,浏览器也会重新请求所有的 js 代码。 image.png

  5. 很明显这个结果并不是我们期望的,此时我们就会想到,把第三方库的代码和自己的代码分包来进行最终的打包。(vendor 这个单词的含义就是小贩、供货商,而在这个场景下指的就是第三方库) image.png

  6. 当用分包模式的时候,我们自己代码更新后由于 hash 值变化,浏览器会重新发起新的请求,而 vendor 的第三方库代码,就可以配合服务器来进行 304 协商缓存减少不必要的网络带宽。 image.png

  7. 在 vite.config 中同样可以设置。 import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue';

    // https://vite.dev/config/
    export default defineConfig({
      plugins: [vue()],
      build: {
        rollupOptions: {
          output: {
            manualChunks: (id) => {
              if (id.includes('node_modules')) return 'vendor';
            },
          },
        },
      },
    });
    
  8. 对于打包结果,两者可以通过配置来达到基本一致的结果。所以在这里我想表达的意思是再次强调 vite 的快并不是指代码执行速度,需要你请理清打包工具这个概念。

三. webpack 的 npm run dev

  • 当我们在 webpack-test 目录下运行 npm run dev 的时候,webpack 内部其实做了很多工作,这里简单概述一下发生了什么:
      1. webpack 是运行在 node 环境中的,首先它会启动 fs 模块来读取你入口文件;
      1. 根据你的入口文件(默认为 src/main.js),来解析你所有模块的相互依赖关系;
      1. 而由于 webpack 本身只支持 js 和 json 文件,所以当解析到 import img from 'asset/icon.png' 等这样的非标准模块时,会调用相关 loader 来协助将它转译成为一个 js 文件,然后 webpack 才可以进行后续的抽象语法树分析;(如下图,通过识别一些特殊的关键字来进一步分析出不同模块的依赖关系) image.png
      1. 当解析完一个模块后,会记录该模块的内容,最后绘制出完整的依赖关系图。这张关系图你可以近似理解为以相对路径为 key,以模块内容为 value 的一个数组。 image.png
      1. 最后将所有的代码整合,放入到一个自执行函数里,自执行函数里会调用 "./src/main.js" 所对应的函数体。最终形成一个 chunk 文件,这个chunk 文件也就是我们对应的最终产物。 image.png
      1. 然后 webpac-dev-server 启动,请求 index.html 文件,解析到 script 标签后,运行自执行函数,随后页面产生。

四. vite 的 npm run dev

  • 当我们在 vite-test 目录下运行 npm run dev 的时候:

      1. 预构建第三方依赖,vite 会扫描 package.json 中所有的运行时依赖,用 esbuildnode_modules/.vite/deps 为这些第三方库打包成类似于上面 vender 一样的临时文件。当开发服务器启动后,遇到浏览器请求相应的资源可以直接使用预构建的结果,并且通过 memory cache 来减少不必要的网络请求。
        image.png image.png
        对应在 network 上的表现如下: image.png
      1. vite 会直接启动一个 http-server,它不会像 webpack 那样需要分析整个依赖关系以后再启动;
      1. 此时浏览器会请求 index.html 文件,解析到 script 标签以后,发现引用了 main.js,于是浏览器会请求 main.js 文件。 image.png
      1. 此时我们的界面又引用了 vue 模块一个 app.vue 组件和一些别的模块,对于浏览器来讲,每一个 import 都会创建一个新的请求去获取资源,浏览器需要什么,服务器就按需返回什么,没用到的浏览器也不会产生请求。 image.png
      1. 那么问题来了,浏览器压根就不认识 App.vue 这种文件,那界面又是怎么产生的呢?当我们打开网络这一栏,你会发现,App.vue 对应请求的返回结果是一个 js 文件,并不是浏览器不认识的所谓的 .vue 文件,这其实是 vite 开发服务器的按需编译的能力实现的。也就是说 vite 的开发服务器并不是一开始就编译好文件,等待浏览器请求,而是当请求到达时,才进行编译然后返回对应的资源代码。 image.png

五. 开发体验差异总结

  1. vite 有使用 esbuild 提前打包依赖的预构建过程,webpack 没有这个优化。

  2. vite 无需提前构建依赖关系,直接启动开发服务器,然后利用浏览器特性按需加载、即时编译模块;而 webpack 需要根据入口文件,进行词法分析,语法分析后生成抽象语法树,获取依赖关系,构建出一个完整的 bundle 才可以启动开发服务器。

  3. vite 检测某个模块内容改变时,通过改变 contenthash 让浏览器重新请求该模块即可,这大大减少了热更新的时间。而 webpack 一个模块或其依赖的模块内容改变时,需要重新编译解析这些模块。