Keeping bundles for each page small

I'm using about 20 slices across 3 custom types for one of my websites. The high number in slices is because i have some typical content management slices but also some to define layout features with page related properties such as related blogs in a sidebar & bottom-bar.

I noticed however, that during build each page is importing all 20 slices which is unnecessary as not all slices are enabled for each custom type, but the doducmented implementation doesn't take in consideration which slices are enabled in which custom type.

As a work-around - and unfortunately a manual work-around so far, I created my own custom SliceZone when I used next-slicezone. With the replacement of next-slicezone with @prismicio/react, I could replace the custom SliceZone witht he standard one, but still need to keep some manually maintained index files.

In essence I have for each custom type and tab(Main, SideBar) one index file that replicates the slices as allowed in the UI. For illustration purpose:

blog-slices.js

import ASlice from './ASlice';
import BSlice from './BSlice';
import CSlice from './CSlice';

export const blogComponents = {
  a_slice: ASlice,
  b_slice: BSlice,
  c_slice: CSlice
};

page-slices.js

import ASlice from './ASlice';
import DSlice from './DSlice';
import ESlice from './ESlice';

export const blogComponents = {
  a_slice: ASlice,
  d_slice: DSlice,
  e_slice: ESlice
};

and in the page, the index file gets imported and provided as the components parameter for SliceZone:

import { blogComponents } from 'slices/slices/blog-slices';

<SliceZone slices={sectionsData} components={blogComponents } />```

Much easier and less code than with next_slicezone and it keeps the bundle size of each page to a minimum. But ideally this should somehow be fully automated...

Hello @peter2, I'll consult this with the team and come back when I have more information.
Thanks

Hey @peter2, I got a response from the DevX team. They suggest two other alternatives that still require manual work but it would be more manageable, as you'd only need to maintain a single file.

  1. With Next.js, components can be lazy-loaded using the dynamic() function from next/dynamic . Advanced Features: Dynamic Import | Next.js
  2. If you're using the latest version of Next.js and have upgraded to React 18, you should be able to use React.lazy(). It works just like next/dynamic, except comes from React itself.

Between the two, we would recommend trying Option 1 ( next/dynamic ) as it is more likely to work. In the future, you could replace next/dynamic with the native React.lazy() once your app is using React 18 (if it isn’t already). You need to create a separate slices/components.js file next to the slices/index.js file that Slice Machine creates. It would look like this:

// slices/components.js

import dynamic from 'next/dynamic';

// List all components, even ones not used for a specific Custom Type.
export const components = {
  a_slice: dynamic(() => import('./ASlice')),
  b_slice: dynamic(() => import('./BSlice')),
  c_slice: dynamic(() => import('./CSlice')),
};

Then, anywhere <SliceZone> is used, pass that components object to the components prop. Individual Slice components will only be loaded if used on the current page.

Just a note, dynamically importing slices is not recommended if your content in those slices is important for SEO. Since these slices are lazily loaded, the Google crawler might skip them when crawling your website, thus losing SEO points.

1 Like

Yes, it would need to be server side lazy loaded. I just looked into the docu and it sounds like there is a way to do this on the server side. My pages are all ISR for SEO purpose.

How are you going to server side lazy load the components in nextjs? Wouldn't this still impact SEO?

For me lazy loading is clearly a client side action and therefore not suitable for SEO. But the next/dynamic docu seems to imply you can load dynamically a module on the server side if needed. This is new to me, but I will try it out.

Interesting. Please keep me up to date as I would be interesting in utilising dynamic loading if it doesn't impact SEO

I'll be damned - it seems to work indeed. Dynamic on server side rather than lazy-loading on client side.

Before:
image

After:
image

I tested a page after the change with Chrome (Javascript turned off) and the slices are shown. It's incredible that the build time for each page reduced by more than 50% and the First Load JS reduced 20%.

Wow that's great! Can you share a code snippet of your slice zone?

Also, worth doing the check recommended in this article to ensure that everything is working correctly - How To Crawl JavaScript Websites - Screaming Frog

I followed Pau's work around number 1:

// slices/components.js

import dynamic from 'next/dynamic';

// List all components, even ones not used for a specific Custom Type.
export const components = {
  a_slice: dynamic(() => import('./ASlice')),
  b_slice: dynamic(() => import('./BSlice')),
  c_slice: dynamic(() => import('./CSlice')),
};

I have now changed all page types and have a single component file with all slices. I see similar positive impact on the First Load JS with the other two types, but strangely the time it took to build a blog page now increased from about 3.5secs to 20secs.
I also need to verify the overall impact on javascript loads aside from First Load JS.

I finished now my tests and here are my findings
Pros:

  • First Load JS is smaller which might have a positive impact on SEO.
  • Maintenance is less as the components file needs changing only when a new slice is introduced. Before that I also have to maintain it when a enable an existing slice for a custom type. Though it's rare.

Neutral:

  • Overall bundle size hasn't changed - apart from that Next loads also bundles for other pages with the lowest priority, it loads.

Cons:

  • Some page types take 5-6 times longer to build. I'm using ISR and have all pages pre-built. At the moment there aren't too many, but once the number increases, this might become a problem.

I came to the conclusion to use a combination of my approach (having for each custom type a component object) and Pau's suggestion to load the slices in those components dynamically. The benefit is that first load JS is smaller and the time it takes to build didn't go up.

1 Like

Hello,

I prepared a small utility script which creates a new index file with dynamically/lazily imported slices based on @Pau's answer.

It reads the original index, matches lines with regex word1: word2, and rewrites it to word1: lazy(() => import('./word2'),. I hope it covers all cases of how the original index may look like, but I'm not fluent in Slicemachine, so CMIIW.

The script runs using node --watch-path and I also added it to our dev script. Whenever a change is detected in the original index file it will run the script and generate the dynamic index file.

// package.json

"scripts": {
  "dev": "concurrently \"npm:next:dev\" \"npm:slicemachine\" \"npm:sync-slice-index\" --names \"next,slicemachine,sync-slice-index\"",
  "sync-slice-index": "node --watch-path slices/index.ts syncSliceIndex.mjs"
}
// syncSliceIndex.mjs

import { readFile, writeFile } from 'fs/promises'

const STATIC_INDEX_PATH = './slices/index.ts'
const DYNAMIC_INDEX_PATH = './slices/dynamicIndex.ts'

const convertSlicesIndex = (file) => {
  const mappings = [...file.matchAll(/(\w+:) (\w+),/g)]

  const dynamicMappings = mappings.map(
    mapping => `  ${mapping[1]} lazy(() => import('./${mapping[2]}')),`
  )

  return [
    'import { lazy } from \'react\'',
    '',
    'export const components = {',
    ...dynamicMappings,
    '}',
    ''
  ].join('\n')
}

// JOB START
console.info(`Updating '${DYNAMIC_INDEX_PATH}'.`)

const file = await readFile(STATIC_INDEX_PATH, 'utf8')
const dynamicIndexFile = convertSlicesIndex(file)

await writeFile(
  DYNAMIC_INDEX_PATH,
  dynamicIndexFile,
  { encoding: "utf8" }
)

console.info(`Successfully updated the dynamic index of Slices.`)

1 Like