Effortless Hydration and Efficient Code Splitting in NextJS Application

I understand Lazy Hydration and Code Splitting, but how can I ensure that the split chunk is only downloaded when the component is being hydrated?

This is what my code currently looks like

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 is rendered below the fold, meaning it will only hydrate once the user scrolls. However, the JavaScript chunk for MyComponent is downloaded immediately as the page loads.

I found a workaround by using dynamic import only on the client side, but this causes the component to momentarily disappear upon hydration. This leads to increased Cumulative Layout Shift (CLS) which is not acceptable. Here's the code for this workaround

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

Please note that I want to render the component's HTML during server-side rendering. This is why I don't want to lazy load it. I aim to lazy hydrate so that the component's HTML is displayed initially but its JS is only downloaded and executed when visible.

Answer №1

Revision:

Within the document:

// Disables preloading of code-split chunks
class LazyHead extends Head {
  getDynamicChunks(files) {
    const dynamicScripts = super.getDynamicChunks(files);
    try {
      // Retrieve 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 original dynamic scripts
      return dynamicScripts;
    }
  }
}

const backupScript = NextScript.getInlineScriptSource;
NextScript.getInlineScriptSource = (props) => {
  // Do not allow next to load all dynamic IDS, let webpack handle it
  if (props?.__NEXT_DATA__?.dynamicIds) {
    const filteredDynamicModuleIds = props?.__NEXT_DATA__?.dynamicIds?.filter(
      (moduleID) => !moduleID?.startsWith('lazy'),
    );
    if (filteredDynamicModuleIds) {
      // Modify dynamicIds in next data
      props.__NEXT_DATA__.dynamicIds = filteredDynamicModuleIds;
    }
  }
  return backupScript(props);
};

In next configuration

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;
  },
});

In file, using next/dynamic

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

Answer №2

Unsure if this is what you're looking for, I've implemented a technique that combines lazy hydration with a webpack plugin and custom next head in order to preserve the HTML while removing below-the-fold dynamic imported scripts. This way, I only download the necessary JavaScript and hydrate the component right before it becomes visible to the user as they scroll. It doesn't matter if the component was used during the initial render - I don't need the runtime unless it's going to be seen.

This method is currently being used in production and has cut down the initial page load time by 50% without any negative impact on SEO.

If you're interested in how to implement this, feel free to contact me on Twitter @scriptedAlchemy. While I haven't written a post about it yet, I can assure you that achieving a "download as you scroll" design is quite achievable with minimal effort.

Answer №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>
    </>
  );
};

The provided code snippet enables downloading the component upon clicking a button. If you prefer the download to occur when scrolling to a specific position, consider using react-intersection-observer to trigger setDownloadComp. While I have not personally utilized react-lazy-hydration, my experience involves employing react-intersection-observer and nextjs dynamic imports for lazily loading components based on user scroll behavior.

Answer №4

If you're looking to simplify a certain task, I've developed a library that can help. Here are some benefits you can enjoy:

  • Complete HTML page rendering (great for SEO)
  • Loading JS and Hydrate only when necessary (ideal for PageSpeed)

Wondering how to make use of it?

import lazyHydrate from 'next-lazy-hydrate';

// Utilize lazy hydration on scroll into view
const WhyUs = lazyHydrate(() => import('../components/whyus'));

// Use lazy hydration when users hover into the view
const Footer = lazyHydrate(
  () => import('../components/footer', { on: ['hover'] })
);

const HomePage = () => {
  return (
    <div>
      <AboveTheFoldComponent />
      {/* ----The Fold---- */}
      <WhyUs />
      <Footer />
    </div>
  );
};

Find out more here: https://github.com/thanhlmm/next-lazy-hydrate

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for bypassing arrow functions when sending prop values to another component?

**Stateful ApplicatorType Component** class ApplicatorType extends Component { public state = { applicatorTypes: ['Carpenter', 'Painter', 'Plumber'], applicatorTypesSelected: [], } public render() { allotedTypes = ( &l ...

Is d3 Version pretending to be a superior version?

I have encountered an issue with my project that involved using d3 v5.5.0. After transferring it to a different computer and running npm install, the application now seems to be recognizing d3 as a higher version? A crucial part of my program relies on th ...

When refreshing, Next.js and Styled Components may lose synchronization between the server and the client

My Next.js app utilizes styled components. Upon the initial load of any page, everything appears properly styled with no issues. However, upon refreshing a page, I encounter a console error that reads: Warning: Prop `className` did not match. Server: &quo ...

Can Enzyme snapshots be utilized with React fragments?

Are React fragments compatible with Enzyme's snapshots? Currently, it appears that fragments from React 16+ are being displayed as Symbols in enzyme's shallow() method, leading to a conversion error: "TypeError: Cannot convert a Symbol value to a ...

Refresh Next.js on Navigation

Is there a way to trigger a reload when clicking on a Link component from next/link? I attempted to create my own function within the child div of the link that would reload upon click. However, it seems to reload before the route changes and is not succ ...

Encountering issues with dependencies while updating React results in deployment failure for the React app

Ever since upgrading React to version 18, I've been encountering deployment issues. Despite following the documentation and scouring forums for solutions, I keep running into roadblocks with no success. The errors displayed are as follows: $ npm i np ...

The type 'TaskListProps[]' cannot be assigned to type 'TaskListProps'

I'm struggling with handling types in my TypeScript application, especially with the TaskListProps interface. export default interface TaskListProps { tasks: [ { list_id: string; title: string; description: string; status ...

Steps to transform current react application into an npm module

I successfully created a react app utilizing react router, react redux, and saga. My goal is to export this project as an npm package so that its containers can be utilized in my other projects while maintaining all the functionalities. How can I achieve t ...

Struggling to transfer sass variables between scss files

Within my React application, I have a specific file named _variables.scss which holds various defining variables to be utilized across different .scss files: $navy: #264653; $green: #2A9D8F; $yellow: #E9C46A; $orange: #F4A261; $red: #E76F51; $title-font: ...

Stop using Flexbox in Material UI on small screens

How can I prevent Flexbox from functioning on small screens using Material UI? I am looking for something similar to the d-md-flex class in Bootstrap but for Material UI. In Bootstrap (works as expected) <div class="d-md-flex align-items-md-center ...

Refresh the Data Displayed Based on the Information Received from the API

As someone who is relatively new to React, I have been making progress with my small app that utilizes React on the frontend and a .NET Core API on the server-side to provide data. However, I have encountered a problem that I've been grappling with fo ...

Shifting the Dropdown Indicator in react-select: A Step-by-Step Guide

Any ideas on how to make the Dropdown Indicator "float" to the left (instead of the default right) on a React Select Component? I've been exploring the Dropdown Indicator properties in its API, but so far no luck... ...

Displaying input fields in editable cells of a react-table should only happen upon clicking the cell

I've been experimenting with the react-table library and my goal is to create editable cells that only show an input box for editing when clicked. Unfortunately, I'm running into an issue where the input box appears constantly instead of just on ...

Launching a React build using Docker on Apache or AWS

I am a beginner to the world of docker and react. My goal is to successfully deploy a production build on AWS. Here are the steps I have taken so far: Created a dockerfile. Built and ran it on the server. npm run and npm run build commands are functionin ...

React Native state remains unchanged

I've been struggling for hours trying to pass a string from AsyncStorage to a state variable using hooks, but I can't seem to get it to work. Here is the code snippet: const [cartitems, cartitemsfunc] = useState(''); const d = async () ...

Prevent dialog from shifting when mobile keyboard appears in React using Material-ui

I am facing a challenge with a dialog that includes multiple text fields for user input, like the illustration provided in this link. https://i.stack.imgur.com/c2gIY.png The issue arises when the user interacts with these text fields on a mobile device, c ...

What is the method to customize the color of the Stepper component in React Material UI?

The image shows my attempt to modify the step color based on its status: green for correct, yellow for in-progress, and red for incorrect. Can anyone guide me on how to achieve this? ...

Having trouble sending a picture to AWS S3 from a NextJS app when running on the server

I've encountered an issue with my NextJS application. Uploading files from localhost works perfectly fine, but the problem arises when attempting to upload remotely. I have hosted my app on Github Pages and whenever I try to upload files remotely, I a ...

Sorting feature in Antd table does not function properly when using render method

I'm having trouble sorting data in a table because the sorter with render function only works once. { title: 'App', dataIndex: 'location', render: location => location.join(', '), sorter: true }, { title: &a ...

Implementing a feature in React/NextJS to selectively load an external script for users who are not authenticated

Our goal is to dynamically load an ad script only if a user is not authenticated. Please note that our project is built with Next.js. Here is a simplified example of how we are currently implementing the ad script loading functionality: const AdContext = ...