Attempting Code Splitting, or reducing my bundle size

Hi,

I have a Meteor website that on a experimental branch to explore code splitting with SSR I have introduced this package → https://github.com/CaptainN/npdev-react-loadable.

Previously I had used lazy and suspense from React to load pages in my routes file but SSR didn’t work, when I viewed the source of the page I just got my loading component, and I want to return all the html in the body for SEO.

After switching to npdev-react-loadable this seemed to reduce my bundle size, currently the bundle size is 2.7mb, it used to be 3.7mb but removing some packages and implementing react-loadable seems to help.

I am wondering if there are other ways to reduce my bundle size, the node_modules seem to be a majority of the issue. The largest module on the homepage is react-strap which is buried in another component I tried to only include relevant code for now.

Currently on this experimental branch I have this client/index.js file

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { Router } from 'react-router-dom';
import routes from '../both/routes';
import { preloadLoadables } from 'meteor/npdev:react-loadable';


preloadLoadables().then(() => {
	const container = document.getElementById('app');

	const html = hydrateRoot(
		container,
		<>
			<Router history={history}>
				<div>{routes}</div>
			</Router>
		</>,
	);
});

and this server/index.js file

import React from 'react';
import { renderToString } from 'react-dom/server';
import { onPageLoad } from 'meteor/server-render';
import { StaticRouter } from 'react-router';
import { Helmet } from 'react-helmet';
import { ServerStyleSheet } from 'styled-components';
import {
	LoadableCaptureProvider,
	preloadAllLoadables,
} from 'meteor/npdev:react-loadable';

preloadAllLoadables().then(() => {
	onPageLoad(async (sink) => {
		const context = {};
		const sheet = new ServerStyleSheet();
		const routes = (await import('../both/routes.js')).default;
		const loadableHandle = {};

		const App = (props) => (
			<StaticRouter location={props.location} context={context}>
				{routes}
			</StaticRouter>
		);

		const html = renderToString(
			<LoadableCaptureProvider handle={loadableHandle}>
				<App />
			</LoadableCaptureProvider>,
		);
		sink.renderIntoElementById('app', renderToString(html));
		sink.appendToBody(loadableHandle.toScriptTag());
		sink.appendToHead(sheet.getStyleTags());
		const helmet = Helmet.renderStatic();
		sink.appendToHead(helmet.meta.toString());
		sink.appendToHead(helmet.title.toString());
		sink.appendToHead(helmet.link.toString());
	});
});

Now I created a Loadable component at Loading/Loadable.js

import React from "react"
import {Loadable} from 'meteor/npdev:react-loadable';


const LoadableComponent = opts => Loadable({
	loading: () => <p>Loading</p>,
	...opts
});

export default LoadableComponent;

And in my routes.js I am loading my homepage component with the loadableComponent

const HomePage = LoadableComponent({
	loader: () => import('../../ui/containers/Pages/Homepage.container'),
})

and then in that same file calling it in the route

export default (
	<Switch>
		<Route exact name="index" path="/" component={HomePage} />
       ...

Now on that homePage component I have the following

import LoadableComponent from '../Loading/Loadable';

const PageWrapper = LoadableComponent({
	loader: () => import('../Global/PageWrapper'),
});

const StickyFooter = LoadableComponent({
	loader: () => import('../Global/StickyFooter'),
});

export default function HomePage(props) {
	const [state, setState] = React.useState({});
	let page = false;

	if (Meteor.isServer) {
		page = props.page;
	}
	if (Meteor.isClient) {
		React.useEffect(() => {
			if (props) {
				setState({ ...props });
			}
		}, [props]);

		page = state.page;
	}

	if (page) {
		return (
			<PageWrapper
				noMargin
				type={'page'}
				user={props.user}
				page={page}
			>
				<main
					itemType="https://schema.org/Organization"
					className="position-relative"
				>	
				</main>			
			</PageWrapper>
		);
	}
}

// export default HomePage;

Now what is happening in this experimental branch is has a bunch of html markup like this

&amp;lt;div class=&amp;quot;sc-bCfvAP position-relative&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;sc-dPWrhe bciHYw page-wrapper-container&amp;quot;&amp;gt;&amp;lt;div class=&amp;quot;page-wrapper... more content missing

it seems to have html with character entities but it cuts off at some point and just shows … as seen at the end of this Gist html · GitHub

Is this a limitation of this package.

Previously my two server/index.js and client/index.js files looked like this with normal imports and routes, now it is somehow working but is mixing readLoadable and npdev react Loadable packages. I didn’t set it up like this.

server

import React from 'react';
import { renderToString } from 'react-dom/server';
import { onPageLoad } from 'meteor/server-render';
import { StaticRouter } from 'react-router';
import { Helmet } from 'react-helmet';
import Loadable from 'react-loadable';
import { ServerStyleSheet } from "styled-components"

onPageLoad(async (sink) => {
	const context = {};
	const sheet = new ServerStyleSheet();
	const routes = (await import('../both/routes.js')).default;

	const App = props => (
		<StaticRouter location={props.location} context={context}>
			{routes}
		</StaticRouter>
	);

	const modules = [];
	// const html = renderToNodeStream((
	const html = renderToString((
		<Loadable.Capture report={(moduleName) => { modules.push(moduleName); }}>
			<App location={sink.request.url} />
		</Loadable.Capture>
	));


	sink.renderIntoElementById('app', html);
	sink.appendToHead(sheet.getStyleTags());
	const helmet = Helmet.renderStatic();
	sink.appendToHead(helmet.meta.toString());
	sink.appendToHead(helmet.title.toString());
	sink.appendToHead(helmet.link.toString());
});

client

import { onPageLoad } from 'meteor/server-render';
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import { Router } from 'react-router-dom';
import routes from '../both/routes';
import { preloadLoadables } from 'meteor/npdev:react-loadable';

preloadLoadables().then(() => {
	const container = document.getElementById('app');

	const html = hydrateRoot(
		container,
		<>
			<Router history={history}>
				<div>{routes}</div>
			</Router>
		</>,
	);
});

However the reason I am showing the current branch is because it does render the html in the source like so

I guess I have a few questions here.

  1. Why is the previous way of doing this working mixing react-loadable and npdev react loadable
  2. What should I do, choose the way I setup before or after experimental branch
  3. What would you recommend to tackle code splitting or reducing my bundles size and properly setting up my server and client app rendering.

Any help would be greatly appreciated.

I have the same challenge to get my bundle size smaller than 3mb, which is a lot on mobile devices with weak connection.

The biggest part is the meteor-node-stubs which consumes 10% of my bundle, and it does not seem to tree-shake well. If anyone has an idea how to optimize this, I would love to hear it.

I also tried SSR, but this did not help much either. I ended up with a weird mix of pages and CSS, which I could track down to ConnectedRouter. If this component was in place, the children did not receive the correct route. Also, I had to patch a lot of components because they now also had to work on the server-side as well. When I then saw that the Lighthouse rating was even worse than without SSR, I gave up because all the hassle just wasn’t worth it.

Maybe I’m just to dumb, but implementing SSR into an existing app isn’t that straightforward as I hoped it was. And it doesn’t work for logged-in users either, at least not without a lot of workarounds.

Besides: It doesn’t seem as if you’re using meteor/communitypackages:react-router-ssr. This got me quite far. I’m wondering why this package is not mentioned in the Meteor guide?

You have to avoid using it. First, you need to know which part of your code requires it. Next, figure out how to change your code to work in the client without utilizing this package. In many cases in our use cases, we mistakenly include server-only code with the client bundle.

It does not do code-splitting and lazy-loading components in the client. I believe it only does SSR.

I did not have the time to read the code you posted, but we have succeeded with code splitting using npdev-react-loadable with SSR. This package has a big caveat: it cannot do nested code-splitting.

1 Like

By the way for the missing content in the source I mentioned I had to remove some Meteor.isClient checks and some nested LoadableComponent calls. So I think that solves a large portion of the problem.

For example we had to go from this

{Meteor.isClient && (
	<Waypoint
		scrollableAncestor={window}
		onEnter={() => setIsVisible(true)}
		onLeave={() => null}
	>...

→ To this

useEffect(() => {
    // Set isClient to true after the component is mounted on the client-side
    setIsClient(true);

    // Cleanup function to set isClient back to false when the component is unmounted
    return () => {
      setIsClient(false);
    };
  }, []); // Pass an empty dependency array to run this effect only on mount and unmount

return (	
	<Waypoint
		scrollableAncestor={isClient ? window : undefined}
		onEnter={() => setIsVisible(true)}
		onLeave={() => null}
	>...

Finally I am wondering about the character entities instead of <div> tags and other element tags in the view source is it ok for SEO to view that?

is not necessary if your initial state was false. When a component is being unmounted the local state is reset (deleted) and reinitializes as the component mounts again,

Based on my previous server and client hydration code if someone could help with these warnings and errors in the console

I get the following

warning: Expected server HTML to contain a matching <div> in <div>.
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.
p

I feel like my implementation of npdev react-loadable is not proper or something is wrong.

Like I have said I get character entities in the view source instead of div tags, I feel that is partially where the issue lies.