捆绑优化

捆绑优化是指决定哪个符号放入哪个捆绑包的过程,以便应用程序可以共同定位一起使用的代码。将符号共同定位可以加快应用程序的加载速度。

符号 vs 块

符号是 Qwik 中的单个可延迟加载的部分。每当您在源代码中使用 __$(__) 时,就会创建一个符号。

例如,下面的代码从 component$onClick$ 创建了两个符号。

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Increment {count.value}
    </button>
  );
});

优化器将上述代码重写为以下内容:

文件:chunk-1.js

export default componentQRL(qrl('./chunk-1.js', 's_ABC123'));
 
export const s_ABC123 = () => {
  const count = useSignal(0);
 
  return (
    <button onClickQRL={qrl('/.chunk-1.js', 's_XYZ342')}>
      Increment {count.value}
    </button>
  );
};
 
export const s_XYZ432 = () => {
  const [count] = useLexicalScope();
  return count.value++;
}

在上面的示例中,所有符号(s_ABC123s_XYZ432)都共同定位在同一个块(./chunk-1.js)中。

块是可以包含一个或多个符号的 JavaScript 捆绑包。

最佳符号分布

您可以将捆绑优化视为一个滑块,允许我们优化符号的传递。

  • 在滑块的一端,我们有一个包含所有符号的单个块。这相当于没有任何类型的延迟加载的应用程序。(这是大多数应用程序今天的写法。)
  • 在滑块的另一端,我们为每个符号单独创建一个块。这是 Qwik 应用程序在开发过程中的行为。每个符号都在自己的块中。

单个块的问题是:

  • 它将包含客户端不需要的许多符号。(浪费带宽。)
  • 客户端在加载整个块之前无法运行任何符号。

每个符号单独创建一个块的问题是:

  • 客户端将不得不发出许多请求来加载所有块,通常会导致不理想的瀑布请求。

最佳解决方案在中间某个位置。我们希望有少量的块,但我们也希望将一起使用的符号共同定位在同一个块中。拥有少量的块允许我们优先加载块的顺序,但同时也分摊了进行 HTTP 请求的成本。将符号共同定位可以最小化瀑布。

好消息是,使用 Qwik,您完全可以控制哪个符号放入哪个块。通常,为了进行延迟加载,拆分应用程序需要开发人员编写动态导入并重构代码。在 Qwik 中,所有 $() 都是潜在的延迟加载位置,只需要告知捆绑器如何分发符号即可。

qwikVite() 插件

qwikVite() 插件在您的 vite.config.ts 中控制符号分布。通常,entryStrategy 设置为 smart,允许 Qwik 根据启发式猜测来确定符号应如何进行延迟加载。但是,可以通过在 vite.config.ts 文件中提供 manual 配置来覆盖启发式猜测,如下所示:

export default defineConfig(() => {
  const routesDir = resolve('src', 'routes');
  return {
    // ...
    qwikVite({
      entryStrategy: {
        type: 'smart',
        manual: {
          ...bundle('bundleA', [
              's_I5CyQjO9FjQ',
              's_NsnidK2eXPg',
              's_kDw0latGeM0',
          ]),
          ...bundle('bundleB', [
              's_vXb90XKAnjE',
              's_hYpp40gCb60',
          ]),
          ...bundle('bundleC', [
              's_AqHBIVNKf34',
              's_oEksvFPgMEM',
              's_eePwnt3YTI8',
          ]),
        },
      },
    }),
  };
});
 
function bundle(bundleName: string, symbols: string[]) {
  return symbols.reduce((obj, key) => {
    // Sometimes symbols are prefixed with `s_`, remove it.
    obj[key.replace('s_', '')] = obj[key] = bundleName;
    return obj;
  }, {} as Record<string, string>);
}

那么问题就变成了如何获取诸如 s_I5CyQjO9FjQ 这样的符号名称?请参阅下一节运行时分析。

运行时分析

要解决的根本问题是,静态地确定最佳捆绑包是不可能的。理想的捆绑包将取决于用户的行为。只有在观察用户行为之后,我们才能确定哪些符号是一起使用的。

要从运行中的应用程序收集符号使用情况:

  1. 将以下代码插入到您的应用程序中:
    <script>
      window.symbols = [];
      document.addEventListener('qsymbol', (e) => window.symbols.push(e.detail));
    </script>
  2. 执行一些模拟用户行为的操作。
  3. 打开控制台,输入 symbols 查看使用的符号列表。使用该信息更新 vite.config.ts 文件。

**注意:**我们正在研究在将来创建更好的收集此信息的方法。(请参阅洞察。)

**注意:**符号哈希被设计为在许多次编译中保持稳定。但是,如果代码经历复杂的重构,哈希可能会发生变化。这不会破坏应用程序,但可能会导致符号移动到不同的次优块,直到再次收集运行时分析。

服务工作器

Qwik 应用程序使用服务工作器确保捆绑包被预取到浏览器的缓存中,并且任何用户交互都会导致缓存命中,因此没有延迟的交互。

请参阅推测性模块获取

事件

可以通过监听以下自定义事件来观察有关符号何时加载的所有信息:

qprefetch 自定义事件详细信息。

当通过呈现新的应用程序视图来向用户公开新的代码路径时,将触发 qprefetch 事件。(例如,呈现新的模型对话框将有一个新的按钮。我们希望确保新的按钮代码被预取,以便如果用户与按钮交互,就不会有延迟。) 通常,服务工作器侦听 qprefetch 事件并将符号加载到缓存中。服务工作器具有符号到捆绑包的映射,并使用此信息根据符号确定要预取的捆绑包。

export interface QPrefetchDetail {
  /// 要预取的符号列表。
  symbols: string[];
}

qsymbol 自定义事件详细信息。

每次 Qwik 需要解析符号时,都会触发 qsymbol 事件。监听此事件可以让您了解应用程序何时加载了不同的符号。然后可以使用这些信息通过将需要一起使用的符号放在同一个捆绑包中来更好地优化捆绑包。

export interface QSymbolDetail {
  /// 触发符号解析的可选 DOM 事件。
  element?: Element;
  /// 解析符号时的请求时间。
  reqTime: number;
  /// 要解析的符号。
  symbol: string;
}

瀑布

服务工作器尝试通过预取捆绑包来最小化瀑布。为了能够做到这一点,服务工作器具有符号和块的 manifestmanifest 表示所有符号及其对应块的完整图。它还知道导入图,因此如果预取了一个符号,服务工作器还将预取作为导入图的一部分所需的所有其他符号。

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • mhevery
  • hamatoyogi