Vite performance optimization - increase business code pre-construction, speed up the output of the first screen

content

What makes vite stand out is the pre-built and responsive development experience during the development phase. However, when you use vite in your own project, you may experience acclimatization. A very important problem is that optimization has no effect, mainly because the first screen of our project, that is, index.html, relies on too many business development modules, and requests These modules require network time-consuming, thus blocking the fast first screen output.

In addition, our projects generally use vite to speed up local development, and use stable webpack for production builds, so we don't want to modify the structure of business code and the way to import other modules for vite. This is a big pain point.

a demo

We have the following project

├── index.html
├── login.html
├── package.json
├── src
│   ├── App.jsx
│   ├── components
│   │   ├── Component1.tsx
│   │   ├── Component2.tsx
……
│   │   ├── Component9.tsx
│   │   ├── Component10.tsx
│   │   └── index.ts
│   ├── main.js
│   ├── pages
│   │   ├── home
│   │   │   ├── Index.tsx
│   │   │   └── main.js
│   │   └── login
│   │       ├── Index.tsx
│   │       └── main.js
│   └── utils
│       ├── index.ts
│       ├── util1.ts
……
│       ├── util19.ts
│       ├── util20.ts

├── vite.config.js
└── yarn.lock
复制代码

For components/index all components will be exported:

import Comp1 from './Component1'; 
import Comp2 from './Component2'; 
……
import Comp9 from './Component9';
import Comp10 from './Component10';

export default {
  Comp1,
  Comp2,
  ……
  Comp9,
  Comp10
}
复制代码

For utils/index will import all utils

import util1 from './util1';
import util2 from './util2';
……
import util20 from './util20';

export default {
util1,
util2,
……
util20,
}
复制代码

For index.html, login.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
</head>

<body>
  <div>home</div>
  <div id="app">home</div>
  <script type="module" src="/src/pages/home/main.js"></script>
</body>

</html>
复制代码
// home/main.js
import { createApp } from "vue";
import App from "./Index";
import { toString, toArray } from "lodash-es";

console.log(toString(123));
console.log(toArray([]));

createApp(App).mount("#app");

// home/index.tsx
import { defineComponent } from "vue";
import comps from "../../components/index"; 
import util from '../../utils/index';

console.log(util.util1);


export default defineComponent({
  setup() {
    return () => {
      return <div>
        <comps.Comp1>Comp1</comps.Comp1>
        <a href="/index.html">home</a> &nbsp;&nbsp;
        <a href="/login.html">login</a>
        </div>;
    };
  },
});

复制代码

Use vite default configuration

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

export default defineConfig({
  plugins: [vue(), vueJsx()],
});
复制代码

Starting the project we will get this effect:

ezgif.com-gif-maker.gif

You will see that when you request home/main.js, because index.tsx is referenced and index.tsx references components and utils, these files will be loaded, and all dependent modules in components and utils will also be loaded, but In the actual project, only comp1 and utils1 are used. For other modules, vite does not guarantee that other modules have no side effects, so they will be loaded into them. Affects the time of the first screen output.

再者, 如果跳转到其他页面比如 login.html 时候, 虽然 组件与 util 那么多文件都没有修改, 但是对于 用户模块来说不会进行浏览器强缓存, 具体可以看 vite2 源码分析(二) — 请求资源。 也就是 cache-control no-cache, 会去 vite 服务端再一次请求该模块, 如果该模块没有修改(etag 相同)则返回 304, 否则返回 200, 但是对于这么文件请求,哪怕是 304 也是很消耗时间的。 所以我们需要将那些不经常变动的业务模块进行打包。对于第三方模块 比如 vue lodash-es vite 默认在预构建的过程中会提前打包好的, 但是默认情况下并不会对用户自定义的模块进行预构建处理, 预构建的内容可以参考 vite2 源码分析(一) — 启动 vite

定义成 npm 包通过 link 到 node_modules

// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

export default defineConfig({
  +optimizeDeps: {
  +  include: ["my-components", "my-utils"],
  +},
  plugins: [vue(), vueJsx()],
});

// package.json
  "dependencies": {
    "lodash": "^4.17.21",
    "lodash-es": "^4.17.21",
    "vue": "^3.0.5",
    +"my-components": "link:src/components",
    +"my-utils": "link:src/utils"
  },

// 在 componetns utils 中增加 package.json
{
  "name": "my-components",
  "version": "0.0.1",
  "main": "./index.ts",
  "module": "./index.ts",
  "dependencies": {
    "vue": "^3.2.31"
  }
}
// 修改 pages/home/index.ts

// import comps from "../../components/index";
// import util from '../../utils/index';
import comps from 'my-components';
import util from 'my-utils';
复制代码

也就是说将 components utils 封装成第三方依赖, 然后将 link 到 node_modules中,然后文件中使用的不是相对路径而是包名。

image_ieMAv16116qKLwTr4mC9EH.png

运行以后感觉还不错,components 合并到了 my-component utils 合并到了 my-utils 中, 看起来都是用了内存缓存了。

所以这种方法适用于比如 utils 的没有副作用的包, 否则不建议使用这种方式, 并且这种方式对于一个大型项目来说修改工作量也是致命的。

自定义 vite plugin 解决

一个合理的设计是不应该在业务代码中出现一些为了工程化的优化而带来的一些代码侵入的, 所以上述为了提高开发效率而直接改动源代码的思路应该是错误的。那么我们来从自己写一个项目相关的插件的方式来解决掉这个问题。

//vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
const { resolve, dirname, extname } = require("path");

const getPathNoExt = (resolveId) => {
  const ext = extname(resolveId);
  return ext ? resolveId.replace(ext, "") : resolveId;
};

const myDeps = [resolve(__dirname, "./src/components/index.ts"), resolve(__dirname, "./src/utils/index.ts")];

const importDepsPlugin = () => {
  let server = null;
  return {
    name: "my-import-deps-plugin",
    enforce: "pre",

    configureServer(_server) {
      server = _server;
    },

    resolveId(id, importer) {
      const resolvePath = getPathNoExt(resolve(dirname(importer), id));
      const index = myDeps.findIndex((v) => getPathNoExt(v) === resolvePath);
      if (index >= 0) {
        const cacheDir = server.config.cacheDir;
        const depData = server._optimizeDepsMetadata;
        if (cacheDir && depData) {
          const isOptimized = depData.optimized[myDeps[index]];
          if (isOptimized) {
            return isOptimized.file + `?v=${depData.browserHash}${isOptimized.needsInterop ? `&es-interop` : ``}`;
          }
        }
      }
    },
  };
};


export default defineConfig({
  optimizeDeps: {
    include: myDeps,
  },
  plugins: [importDepsPlugin(), vue(), vueJsx()],
});


复制代码

image_hrsP9Hp2FbcaPdophmAmqZ.png

Configure the optimizeDeps configuration includes field in the vite configuration file, this field is an array, ** each item must be an absolute path**. These files will be built into the cache only after the pre-build. The specific source code is:

 // vite/2.4.1/packages/vite/src/node/optimizer/index.ts 187行
 const include = config.optimizeDeps?.include
  if (include) {
    const resolve = config.createResolver({ asSrc: false })
    for (const id of include) {
      if (!deps[id]) {
        const entry = await resolve(id)
        if (entry) {
          deps[id] = entry
        } else {
          throw new Error(
            `Failed to resolve force included dependency: ${chalk.cyan(id)}`
          )
        }
      }
    }
  }
  
  const createResolver: ResolvedConfig['createResolver'] = (options) => {
  let aliasContainer: PluginContainer | undefined
  let resolverContainer: PluginContainer | undefined
  return async (id, importer, aliasOnly, ssr) => {
    let container: PluginContainer
    if (aliasOnly) {
      container =
        aliasContainer ||
        (aliasContainer = await createPluginContainer({
          ...resolved,
          plugins: [aliasPlugin({ entries: resolved.resolve.alias })]
        }))
    } else {
      container =
        resolverContainer ||
        (resolverContainer = await createPluginContainer({
          ...resolved,
          plugins: [
            aliasPlugin({ entries: resolved.resolve.alias }),
            resolvePlugin({
              ...resolved.resolve,
              root: resolvedRoot,
              isProduction,
              isBuild: command === 'build',
              ssrTarget: resolved.ssr?.target,
              asSrc: true,
              preferRelative: false,
              tryIndex: true,
              ...options
            })
          ]
        }))
    }
    return (await container.resolveId(id, importer, undefined, ssr))?.id
  }
}
复制代码

After scanImports ends, the include passed in by the user needs to be further processed, mainly calling the resolve method defined by vite itself, that is, config.createResolver. This method does not execute the user-defined plugin, but only executes two alias and resolve plugin. . So the pre-built cache can only be generated by passing in the absolute path. We can look at the pre-built metadata.json. (/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3 is the root of the project)

{
  "hash": "5fe06ea4",
  "browserHash": "c0cdb122",
  "optimized": {
    "vue": {
      "file": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/.vite/vue.js",
      "src": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/vue/dist/vue.runtime.esm-bundler.js",
      "needsInterop": false
    },
    "lodash-es": {
      "file": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/.vite/lodash-es.js",
      "src": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/lodash-es/lodash.js",
      "needsInterop": false
    },
    "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/src/components/index.ts": {
      "file": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/.vite/_Users_mt_Documents_storehouse_vite_demo-0-vite-vue3_src_components_index_ts.js",
      "src": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/src/components/index.ts",
      "needsInterop": false
    },
    "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/src/utils/index.ts": {
      "file": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/node_modules/.vite/_Users_mt_Documents_storehouse_vite_demo-0-vite-vue3_src_utils_index_ts.js",
      "src": "/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3/src/utils/index.ts",
      "needsInterop": false
    }
  }
}
复制代码

In order to be able to access the cached content when requesting, we need to add a custom plugin importDepsPlugin, the main purpose of this plugin is to load the resources in the src directory, if the loaded code contains the import of the import component and the url module , you need to convert this import into a path to a prebuilt cache.

So what we get when we request the Index.tsx file is:

import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/pages/home/Index.tsx");
import {createTextVNode as _createTextVNode, createVNode as _createVNode} from "/node_modules/.vite/vue.js?v=c0cdb122";
import {defineComponent} from "/node_modules/.vite/vue.js?v=c0cdb122";
import comps from "/node_modules/.vite/_Users_mt_Documents_storehouse_vite_demo-0-vite-vue3_src_components_index_ts.js?v=c0cdb122";
import util from '/node_modules/.vite/_Users_mt_Documents_storehouse_vite_demo-0-vite-vue3_src_utils_index_ts.js?v=c0cdb122';
console.log(util.util1);
const __default__ = defineComponent({
    setup() {
        return ()=>{
            return _createVNode("div", null, [_createVNode(comps.Comp1, null, null), _createVNode("a", {
                "href": "/index.html"
            }, [_createTextVNode("home")]), _createTextVNode(" \xA0\xA0"), _createVNode("a", {
                "href": "/login.html"
            }, [_createTextVNode("login")])]);
        }
        ;
    }

});
export default __default__
__default__.__hmrId = "ad9a0a10"
__VUE_HMR_RUNTIME__.createRecord("ad9a0a10", __default__)
import.meta.hot.accept(({default: __default})=>{
    __VUE_HMR_RUNTIME__.reload("ad9a0a10", __default)
}
)
复制代码

You will find that the two imported addresses for comps util have all become cached addresses. At this point, we have realized that the construction speed of vite can be improved without changing the business code.

Guess you like

Origin juejin.im/post/7085424254702845960