Skip to content

getStaticPaths re-imports node modules preventing caching remote data in memory #10933

@smnh

Description

@smnh

Bug report

Describe the bug

When running next dev and requesting a page having the getStaticPaths method, node modules required by the page are re-imported, thus preventing caching headless CMS remote data in memory.

Same happens when running next build - modules required by page component that have getStaticPaths method are re-imported for every pre-rendered page. Making it impossible to fetch the whole remote data in a single API request and use it for all pre-rendered pages.

Important Note:
Headless CMS services may limit number of API requests per month and may apply charges when this limit is passed. The development and build of statically generated sites should minimize the usage of headless CMS API and re-fetch the data only when it is changed. The caching and data invalidation logic might be implemented by CMS clients. Additionally headless CMS services have endpoints to fetch the whole data in a single request. Therefore, to decrease the API usage and support caching, I think Next.js should allow importing modules that cache their data in memory and not re-import them every time page is pre-rendered, while running dev server or when building the site.

To Reproduce

Create simple page pages/[...slug].js

import React from 'react';
import pageLayouts from '../layouts';
import cmsClient from '../ssg/cms-client';

class Page extends React.Component {
    render() {
        // every page can have different layout, pick the layout based
        // on the model of the page (_type in Sanity CMS)
        const PageLayout = pageLayouts[this.props.page._type];
        return <PageLayout {...this.props}/>;
    }
}

export async function getStaticPaths() {
    console.log('Page [...slug].js getStaticPaths');
    const paths = await cmsClient.getStaticPaths();
    return { paths, fallback: false };
}

export async function getStaticProps({ params }) {
    console.log('Page [...slug].js getStaticProps, params: ', params);
    const pagePath = '/' + params.slug.join('/');
    const props = await cmsClient.getStaticPropsForPageAtPath(pagePath);
    // If not using JSON.parse(JSON.stringify(props)), next.js throws following error when running "next build"
    // Error occurred prerendering page "/blog/design-team-collaborates". Read more: https://err.sh/next.js/prerender-error:
    // Error: Error serializing `.posts[4]` returned from `getStaticProps` in "/[...slug]".
    // Reason: Circular references cannot be expressed in JSON.
    return { props: JSON.parse(JSON.stringify(props)) };
}

export default Page;

Implement simple singleton CMS client that fetches CMS data and caches it in memory:

class CMSClient {

    constructor() {
        console.log('CMSClient constructor');
        this.data = null;
    }

    async getData() {
        if (this.data) {
            console.log('CMSClient getData, has cached data, return it');
            return this.data;
        }
        console.log('CMSClient getData, has no cached data, fetch data from CMS');
        this.data = await this.fetchDataFromCMS();
        return this.data;
    }

    async getStaticPaths() {
        console.log('CMSClient getStaticPaths');
        const data = await this.getData();
        return this.getPathsFromCMSData(data);
    }

    async getStaticPropsForPageAtPath(pagePath) {
        console.log('CMSClient getStaticPropsForPath');
        const data = await this.getData();
        return this.getPropsFromCMSDataForPagePath(data, pagePath);
    }

    async fetchDataFromCMS() { ... }
    getPathsFromCMSData(data) { ... }
    getPropsFromCMSDataForPagePath(data, pagePath) { ... }
}

module.exports = new Client();

Navigate to any page rendered by [...slug].js, for example /about.
Following logs will be printed on server::

CMSClient constructor
Page [...slug].js getStaticPaths
CMSClient getStaticPaths
CMSClient getData, has no cached data, fetch data from CMS
Page [...slug].js getStaticProps, params:  { slug: [ 'about' ] }
CMSClient getStaticPropsForPath
CMSClient getData, has cached data, return it
  • constructor is invoked - assuming [...slug].js module was loaded for the first time it is OK.
  • [...slug].js calls getStaticPaths - OK according to Runs on every request in development
  • getStaticPaths of the CMS client is invoked, it does not have the cached data because the client was just constructed therefore the getData is called for the first time - OK.
  • [...slug].js calls getStaticProps - OK according to Runs on every request in development
  • getStaticPropsForPath of the CMS client is invoked, it already has cached data so getData returns early returning the cached data - OK

Refresh the page or click a link <Link href="/[...slug]" as="/about"><a>About</a></Link>.
Following logs will be printed on server:

CMSClient constructor
Page [...slug].js getStaticPaths
CMSClient getStaticPaths
CMSClient getData, has no cached data, fetch data from CMS
Page [...slug].js getStaticProps, params:  { slug: [ 'about' ] }
CMSClient getStaticPropsForPath
CMSClient getData, has cached data, return it

As it can be seen the CMS client is constructed again, and every time a page is requested (even thought it uses the same page module), and the same steps related to fetching and caching the data are repeated. This behavior suggest that when page is requested and getStaticPaths is called, it re-imports all modules.

Note: When using getStaticProps without getStaticPaths, the client is not constructed on every request and therefore cached data is used as expected. See link to demo repository below.

Expected behavior

When running next dev server (or next build), the modules imported by a page component should be imported only once and reused to allow them cache remote data in memory.

Page [...slug].js getStaticPaths
CMSClient getStaticPaths
CMSClient getData, has cached data, return it
Page [...slug].js getStaticProps, params:  { slug: [ 'about' ] }
CMSClient getStaticPropsForPath
CMSClient getData, has cached data, return it

System information

  • OS: macOS
  • Browser: Chrome
  • Version of Next.js: 9.3.0

Additional context

I've setup an example repository that I've used to reproduce this issue. It uses Sanity as Headless CMS. The README file has all the info needed to setup Sanity account and import the initial data used by this example site.

https://github.com/stackbithq/azimuth-nextjs-sanity/tree/nextjs-ssg-api
(use nextjs-ssg-api branch)

Note, when loading the root page '/' (pages/index.js) which has only the getStaticProps method and does not have getStaticPaths, the CMS client is not constructed on every request and therefore data cached in memory of the CMS client module is used as expected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions