【问题标题】:Lazy Hydrate + Code Splitting on NextJS appNextJS 应用程序上的 Lazy Hydrate + 代码拆分
【发布时间】:2021-06-14 11:43:37
【问题描述】:

我知道如何进行 Lazy Hydration 并且我知道如何进行代码拆分,但是如何仅在组件进行 hydrating 时才进行拆分的块下载?

我的代码是这样的

import React from 'react';
import dynamic from 'next/dynamic';
import ReactLazyHydrate from 'react-lazy-hydration';

const MyComponent = dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));

export const PageComponent = () => {
  return (
    ...
    <ReactLazyHydrate whenVisible>
      <MyComponent/>
    </ReactLazyHydrate>
    ...
  );
};

MyComponent 呈现在折叠下方,这意味着它只会在用户滚动时才会水合。问题是MyComponent 的 JS 块将在页面加载时立即下载。

我能够通过仅在客户端上使用动态导入来破解它,但这会使组件在水合时消失一秒钟,因为反应不会使用服务器上呈现的 html。它将重新创建 DOM 元素,并且在 JS 块加载之前它将为空。 当元素消失一秒钟时,它会增加页面 CLS,这就是我不能使用这个 hack 的主要原因。 这是这个 hack 的代码

const MyComponent = typeof window === 'undefined'
    ? require('components/my-component').MyComponent
    : dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));

请注意,我想在服务器渲染上渲染组件的 HTML。这不是我不想延迟加载它的原因。我想要 Lazy Hydrate,这样我就可以在服务器上呈现组件的 HTML,但只能下载 并在可见时执行它的 JS。

【问题讨论】:

    标签: reactjs webpack next.js


    【解决方案1】:

    更新:

    在文档中:

    // stops preloading of code-split chunks
    class LazyHead extends Head {
      getDynamicChunks(files) {
        const dynamicScripts = super.getDynamicChunks(files);
        try {
          // get chunk manifest from loadable
          const loadableManifest = __non_webpack_require__(
            '../../react-loadable-manifest.json',
          );
          // search and filter modules based on marker ID
          const chunksToExclude = Object.values(loadableManifest).filter(
            manifestModule => manifestModule?.id?.startsWith?.('lazy') || false,
          );
          const excludeMap = chunksToExclude?.reduce?.((acc, chunks) => {
            if (chunks.files) {
              acc.push(...chunks.files);
            }
            return acc;
          }, []);
          const filteredChunks = dynamicScripts?.filter?.(
            script => !excludeMap?.includes(script?.key),
          );
    
          return filteredChunks;
        } catch (e) {
          // if it fails, return the dynamic scripts that were originally sent in
          return dynamicScripts;
        }
      }
    }
    
    const backupScript = NextScript.getInlineScriptSource;
    NextScript.getInlineScriptSource = (props) => {
      // dont let next load all dynamic IDS, let webpack manage it
      if (props?.__NEXT_DATA__?.dynamicIds) {
        const filteredDynamicModuleIds = props?.__NEXT_DATA__?.dynamicIds?.filter?.(
          moduleID => !moduleID?.startsWith?.('lazy'),
        );
        if (filteredDynamicModuleIds) {
          // mutate dynamicIds from next data
          props.__NEXT_DATA__.dynamicIds = filteredDynamicModuleIds;
        }
      }
      return backupScript(props);
    };
    

    在下一个配置中

    const mapModuleIds = fn => (compiler) => {
      const { context } = compiler.options;
    
      compiler.hooks.compilation.tap('ChangeModuleIdsPlugin', (compilation) => {
        compilation.hooks.beforeModuleIds.tap('ChangeModuleIdsPlugin', (modules) => {
          const { chunkGraph } = compilation;
          for (const module of modules) {
            if (module.libIdent) {
              const origId = module.libIdent({ context });
              // eslint-disable-next-line
              if (!origId) continue;
              const namedModuleId = fn(origId, module);
              if (namedModuleId) {
                  chunkGraph.setModuleId(module, namedModuleId);
              }
            }
          }
        });
      });
    };
    
    const withNamedLazyChunks = (nextConfig = {}) => Object.assign({}, nextConfig, {
      webpack: (config, options) => {
        config.plugins.push(
          mapModuleIds((id, module) => {
            if (
              id.includes('/global-brand-statement.js')
              || id.includes('signposting/signposting.js')
              || id.includes('reviews-container/index.js')
              || id.includes('why-we-made-this/why-we-made-this.js')
            ) {
              return `lazy-${module.debugId}`;
            }
            return false;
          }),
        );
    
        if (typeof nextConfig.webpack === 'function') {
          return nextConfig.webpack(config, options);
        }
    
        return config;
      },
    });
    

    在文件中,使用下一个/动态

        <LazyHydrate whenVisible style={null} className="col-xs-12">
          <GlobalBrandStatement data={globalBrandData} />
        </LazyHydrate>
    

    【讨论】:

    【解决方案2】:

    不确定这是否是您所追求的,但我使用惰性水合作用与 webpack 插件和自定义 next head 混合使用来保留 html,但在折叠动态导入脚本下方去除。所以我只在用户滚动到视图之前下载 JS 和水合组件。不管它在渲染期间使用了组件 - 我不需要运行时,除非用户要看到它。

    目前处于生产阶段,初始页面加载量减少了 50%。对 SEO 没有影响

    如果您需要实现,请在推特上联系我@scriptedAlchemy,我还没有写过关于它的帖子 - 但我可以告诉您,完全有可能轻松实现这种“滚动下载”设计。

    【讨论】:

    • 嗨扎卡里。这个答案可能指向某个方向,但实际上对答案没有任何帮助。添加提到的代码(包括一些解释)可以使它成为一个很好的答案,因为它实际上并没有回答问题。
    【解决方案3】:
    import { useState } from "react";
    import dynamic from "next/dynamic";
    const MyComponent = dynamic(() => import("components/my-component"));
    
    export const PageComponent = () => {
      const [downloadComp, setDownloadComp] = useState(false);
    
      return (
        <>
          <div className="some-class-name">
            <button onClick={() => setDownloadComp(true)}>
              Download the component
            </button>
            {downloadComp && <MyComponent />}
          </div>
        </>
      );
    };
    

    点击按钮后,上面的代码将下载。如果您希望在滚动到该位置的情况下下载它,您可以使用react-intersection-observer 之类的东西来设置setDownloadComp。我没有使用react-lazy-hydration 的经验,但我一直在使用react-intersection-observer 和nextjs 动态导入来延迟加载依赖于用户滚动的组件。

    【讨论】:

    • 感谢您的回答,但这并不是我想要的。这将延迟加载组件,我不希望这样。我想将组件保留在服务器渲染中,并且只对它进行 Lazy Hydrate。这样,我将始终在 HTML 中呈现组件,但该组件的 JS 只会在可见时被下载并运行
    • 哦,对不起,我帮不上忙。可能有人会有答案,或者我们必须等待即将发布的反应。但我相信你可以使用react-intersection-observer 来模仿这一点。我将它用于无限滚动。一旦用户到达某个点,我就会加载下一个内容,除非用户滚动非常快,否则他们不会注意到加载组件。
    • 是的,也许下一个 react 版本会处理这种情况。我仍在寻找相关的 react repo,如果我发现了什么,我会在这里发布
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-09-30
    • 2020-09-26
    • 1970-01-01
    • 1970-01-01
    • 2019-04-28
    • 2019-08-19
    • 2017-02-05
    相关资源
    最近更新 更多