捆绑优化

捆绑优化是指决定哪个符号应该放在哪个捆绑包中的过程,以便应用程序可以将一起使用的代码放在一起。将符号放在一起可以使应用程序加载更快。

符号与块

符号是 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++;
}

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

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

最佳符号分布

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

  • 在滑块的一端,我们有一个包含所有符号的单个块。这相当于没有延迟加载的应用程序。(这是大多数应用程序今天编写的方式。)
  • 在滑块的另一端,我们为每个符号都有一个单独的块。这是 Qwik 应用程序在开发期间的行为方式。每个符号都在它自己的块中。

使用单个块的问题是

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

为每个符号使用单独的块的问题是

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

最佳解决方案介于两者之间。我们希望有少量块,但我们也希望将一起使用的符号放在同一个块中。少量块使我们能够优先考虑块的加载顺序,但同时摊销了发出 HTTP 请求的成本。将符号放在一起可以最大程度地减少瀑布。

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

qwikVite() 插件

vite.config.ts 中的 qwikVite() 插件控制符号的分配。通常,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 应用程序使用服务工作者来确保捆绑包被预取到浏览器的缓存中,并且任何用户交互都会导致缓存命中,因此交互不会延迟。

请参阅 推测模块获取

请注意,服务工作者功能仅在安全上下文(HTTPS)中可用,并且在某些或所有支持的浏览器中可用。请参阅 serviceWorker 属性 API 规范

事件

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

qprefetch 自定义事件详细信息。

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

export interface QPrefetchDetail {
  /// A list of symbols to prefetch.
  symbols: string[];
}

qsymbol 自定义事件详细信息。

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

export interface QSymbolDetail {
  /// Optional DOM event which triggered the symbol resolution.
  element?: Element;
  /// Request time when the symbol was resolved.
  reqTime: number;
  /// Symbol being resolved.
  symbol: string;
}

瀑布

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

贡献者

感谢所有帮助改进本文档的贡献者!

  • mhevery
  • the-r3aper7
  • mrhoodz
  • Craiqser
  • literalpie
  • antoinepairet
  • hamatoyogi