Otimização de desempenho Vite - aumente a pré-construção do código de negócios, acelere a saída da primeira tela

contente

O que destaca o vite é a experiência de desenvolvimento pré-construída e responsiva durante a fase de desenvolvimento. No entanto, quando você usa vite em seu próprio projeto, pode ocorrer aclimatação. Um problema muito importante é que a otimização não tem efeito, principalmente porque a primeira tela do nosso projeto, ou seja, index.html, depende de muitos módulos de desenvolvimento de negócios , e solicitações Esses módulos exigem tempo de rede, bloqueando assim a saída rápida da primeira tela.

Além disso, nossos projetos geralmente usam vite para acelerar o desenvolvimento local e usam webpack estável para compilações de produção, portanto, não queremos modificar a estrutura do código comercial e a maneira de importar outros módulos para vite. Este é um grande ponto de dor.

uma demonstração

Temos o seguinte projeto

├── 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
复制代码

Para componentes/índice, todos os componentes serão exportados:

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

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

Para utils/index irá importar todos os utils

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

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

Para 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 a configuração padrão do vite

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

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

Iniciando o projeto, obteremos este efeito:

ezgif.com-gif-maker.gif

Você verá que ao solicitar home/main.js, porque index.tsx é referenciado e index.tsx referencia componentes e utils, esses arquivos serão carregados e todos os módulos dependentes em components e utils também serão carregados, mas no projeto real, apenas comp1 e utils1 são usados.Para outros módulos, vite não garante que outros módulos não tenham efeitos colaterais, então eles serão carregados neles. Afeta a hora da saída da primeira tela.

再者, 如果跳转到其他页面比如 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 a configuração do optimizeDeps inclui o campo no arquivo de configuração do vite, este campo é um array, ** cada item deve ser um caminho absoluto**. Esses arquivos serão incorporados ao cache somente após a pré-compilação. O código-fonte específico é:

 // 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
  }
}
复制代码

Após o término do scanImports, o include passado pelo usuário precisa ser processado, principalmente chamando o método resolve definido pelo próprio vite, ou seja, config.createResolver. Este método não executa o plugin definido pelo usuário, mas executa apenas dois alias e resolver plugin. Portanto, o cache pré-construído só pode ser gerado passando o caminho absoluto. Podemos ver o metadata.json pré-criado. (/Users/mt/Documents/storehouse/vite/demo-0-vite-vue3 é a raiz do projeto)

{
  "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
    }
  }
}
复制代码

Para poder acessar o conteúdo em cache ao solicitar, precisamos adicionar um plugin customizado importDepsPlugin, o principal objetivo deste plugin é carregar os recursos no diretório src, caso o código carregado contenha a importação do componente import e o módulo url , você precisa converter essa importação em um caminho para um cache pré-construído.

Então, o que obtemos quando solicitamos o arquivo Index.tsx é:

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)
}
)
复制代码

Você descobrirá que os dois endereços importados para o comps util se tornaram endereços em cache. Neste ponto, percebemos que a velocidade de construção do vite pode ser melhorada sem alterar o código do negócio.

Acho que você gosta

Origin juejin.im/post/7085424254702845960
Recomendado
Clasificación