Vite慢在哪

用了一段时间Vite之后, 开发时确实很快, 体验很棒, 但美中不足的是Vite开发服务器冷启动和启动后第一次加载页面的速度还有待提高。

这里需要区分一下:
冷启动是指项目启动开发服务器时, node_module/.vite下没有任何之前的预构建文件, 这一般是项目第一次启动, 或项目通过server.force启动, 也可能是你手动删除了node_module/.vite之后启动, 此时Vite需要扫描项目的依赖并使用esbuild对这些依赖进行预构建NPM Dependency Resolving and Pre-Bundling

而启动后第一次加载页面则是每次开发服务器启动都会发生, 这时浏览器需要加载当前页面用到的所有文件, 没有缓存。
所以, 需要提速的就是依赖预构建和过多的浏览器请求了。

依赖预构建

对于依赖预构建, 目前(2022/02/18), vite团队正在推进一个非阻塞依赖预构建的特性, 其目标是:

  1. 预构建不阻塞服务器冷启动:
    对于冷启动, 目前需要等待依赖解析和依赖预购建, 该特性完成后则只需要等待依赖解析, 预购建工作将在后台完成, 不会阻塞服务器启动。
  2. 稳定依赖:
    如果某个依赖预构建得到的bundle文件与之前的一样, 就认为这个依赖是稳定的, 这种情况下不会需要完全重新加载页面。
  3. 预构建时不阻塞后续请求:
    现在Vite会在依赖预构建时阻塞所有的请求, 这可能会导致预构建完成后才发现后续请求中存在新的依赖需要进行预构建(比如动态import一个依赖), 进而导致浏览器在冷启动时经历多轮预构建/全量刷新。该特性可以允许请求进入管道来增加Vite发现新的需要预构建的依赖的机会, 提高冷启动的速度。

这个特性目前仍在开发中, 值得期待, 他对于改善冷启动速度会有非常大的帮助。

浏览器请求过多

至于浏览器请求过多, 因为Vite在开发时不进行打包嘛, 随着项目的增长, 虽然有预构建可以把依赖预先打包(否则一个lodash就上百个请求了), 但src下项目内的文件可能也会越来越多, 比如在我们的一个项目中, 首屏加载需要用到800多个文件(当然, 这里我们也有很大的问题, 后面会提到), 这些文件会在加载页面时一次性请求进来, 每一个文件都需要一个完整的GET请求, 加之开发服务器如果使用了HTTP1.1, 并行的http连接还会有限制(chrome下同一个主机名同时最多6个连接), 这就很易造成网络堵塞了。

关于这点, 尤大也提到过Persistent Cache, 他的解决方案是在服务器关闭的时候把内存中的转换缓存到磁盘上, 在下一次服务器启动时再读取并hydrate到模块图中, 这样可以使开发服务器在重启后就能发送304请求来使浏览器使用缓存。

到目前这个问题还在讨论中, 而在Vite实施这个特性之前, 我们也使用了一些方法来减少浏览器请求, 优化加载速度。

代码分割, 按需加载

使用import()进行按需加载, 这样首屏的请求就直接减少了, 这个我在迁移到Vite之前已经做过了。
但代码分割还需要考虑到用户体验:分的太粗首屏加载会更慢, 分的太细又会造成体验的割裂, 所以我们只是对几个主要功能模块进行了分割。

前面提到过我们首屏需要加载800多个文件, 其实就在已经进行了代码分割之后的数字了🤦‍♂️... 那么为了开发时提速, 就应该考虑只在开发时进行更细粒度的代码分割了。

为此我写了一个简单的Vite插件vite-plugin-auto-lazybundle - npm (npmjs.com), 基本原理就是通过配置module id和deps来自动将模块中的依赖转为动态加载:

// vite.config.ts
import autoLazy from 'vite-plugin-auto-lazybundle'

export default defineConfig(({ command, mode }) => {
  const config: UserConfig = {...}
  
  // 仅在开发时
  if(mode === 'development') {
      config.plugins.push(autoLazy({
         // foo.ts: 需要自动按需加载处理的模块 id
         // bar.ts: foo.ts的依赖, 将被转为按需加载
        'foo.ts': [{name: 'bar.ts', path: 'path/to/bar.ts'}]
      }))
  }
  return config;
});

在写了很长的配置之后, 我成功的把项目在开发时的首屏加载请求数...降到了500😂, 其实还可以更低, 但这种做法同样会导致开发时的体验下降: 分的太细, 哪怕在开发时第一次打开一个页面也需要等一下。

使用http2

http2通过二进制分帧, 在一个TCP连接上实现了多路复用, 非常适合处理这种大量请求的情况,如果Vite开发服务器使用http2也可以提高加载页面的速度。(关于http2的更多信息与本文无关, 这里就不再赘述)

其实我一直认为Vite在配置server.https后默认是http/2 + TLS,但在上面优化的过程中我发现开发服务器实际上是在http/1.1 + TLS的状态, 仔细看了文档(Configuring Vite | Vite (vitejs.dev))之后才注意到, Vite在配置了server.proxy后就会从http/2降级为http/1.1。

这点在源码中也有提到:

// node/http.ts
if (proxy) {
    // #484 fallback to http1 when proxy is needed.
    return require('https').createServer(httpsOptions, app)
  } else {
    return require('http2').createSecureServer(
      {
        ...httpsOptions,
        allowHTTP1: true
      },
      app
    )
  }

从源码中提到的这个issue(vitejs/vite#484)可以了解到, Vite用来提供代理功能的库http-proxy不支持http/2(github.com/vitejs/vite…

我们的项目中就恰好是同时使用了https和proxy, 那么如何在这种情况下让Vite开发服务器使用http2呢,显然, 根据上面的源码, 我们需要通过其他方式配置代理。

这里我选择的是使用Vite插件swandir/vite-plugin-http2-proxy: Vite proxy with HTTP2 support (github.com), 把之前配置在server下的代理移到插件配置中即可:

// vite.config.ts
import proxy from 'vite-plugin-http2-proxy';

export default defineConfig(({ command, mode }) => {
  const config: UserConfig = {
    ...
    plugins: [proxy({
        '/api': {
          target: 'http://127.0.0.1:1234/',
          rewrite: path => path.replace(/^\/api/, '/'),
        })],
    ...
  }
  return config;
});

开启http2之后浏览器加载的速度可以进一步提升, 在我的实践中可能因为请求数"比较少", 加载速度的差异其实并不明显, 但http/2从理论上来说是肯定快于http/1.1的, 项目规模越大, 请求数越多这个差异应该会越明显。

总结

说起来其实冷启动跟首次加载就算慢一点问题也不大,还能让大脑放空一会😂,毕竟后面真正开发时增量更新的体验是很棒的。但是既然迁移到Vite的主要诉求就是提供更爽快的开发体验,能让冷启动跟首次加载更快也就更完美了是不?


JEECG低代码平台
664 声望84 粉丝