Loadable-components: Get CSS and JS for requested chunk at server itself

Created on 18 May 2019  ·  17Comments  ·  Source: gregberge/loadable-components

When a route is requested from server, I need to find all the required style and bundle required for that route.

import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server';
const statsFile = path.resolve('./loadable-stats-web.json');

const extractor = new ChunkExtractor({ statsFile }); // Do I need to pass more params here to get my case working?
const scriptTags = extractor.getScriptTags();
const styleTags = extractor.getStyleTags();

extractor.getRequiredChunksScriptContent() // . This is giving empty array.

This gives me script and style tags for base and vendor bundle. BUT the requested chunk's JS and CSS are not included here. It's this base.js bundle when loaded on browser makes request to corresponding chunk.

How we can achieve that in such a way that loadableReady includes that chunk's JS and CSS as well for that loadableReady( ofcourse on client)?

import { loadableReady } from '@loadable/component';

I can do the same in react-loadable

import { getBundles } from "react-loadable/webpack";
const stats = require("./react-loadable-web.json");

getBundles(stats, modules);

This gives me the required chunk's JS and CSS but the limitation is it doesn't include base and vendor bundle's JS/CSS

and hence, opposite to loadable-components, Loadable.preloadReady won't include main and vendor bundle's CSS and JS to load.

import Loadable from "react-loadable";
Loadable.preloadReady()

So, for react-loadable, I've to implement loading vendor and base bundle before chunk's JS/CSS. But not sure how to know the chunk in case of loadable-components while at server

Note: I'm having only one entry point for JS base. style for base's CSS are extracted using mini-css-extract plugin. And vendor is split using webpack's Splitchunks.

Most helpful comment

@aseem2625 react-loadable have issue tab closed and only merge PR that modifies docs. The project is literally dead. It is not compatible with new features coming into React and not compatible with last versions of webpack. But yeah you could use it if you want.

All 17 comments

I'll simplify my query. I'm able to make SSR work using "loadable-components".

When the app loads in the browser, it's the main bundle which inserts the requested route's chunk in DOM, as I'm not able to get the requested chunk on server.

This works fine but I need to get requested route's chunk's JS+CSS file on server side itself. How can we do that?

const scriptTags = extractor.getScriptTags();
const styleTags = extractor.getStyleTags();

This only gives me vendor.js and main.js and main.css

@Rid
Would have take 4-5min to read my query. I said chunking is working.

Anyways, ignore. I found in some article saying that this lib behaves in same manner as in my post. So, using react-loadable for my purpose.

@aseem2625 you should not use react-loadable, but anyway if it fits your needs, stick with it.

@aseem2625 did you tag me in by a mistake? I'm not the maintainer of this project. We're using it in https://github.com/wellyshen/react-cool-starter

Yeh, right @Rid. Actually, you referenced it in the wrong issue, so I tagged you. So, I just wanted to tell that this is not issue with setting up SSR with loadable-components, just that probably this lib doesn't support what I was expecting (, or probably, I couldn't find a way to do so, or probably neoziro have reasons for not having it) 😊

@neoziro I'm not sure why shouldn't use react-loadable? I also actually noticed that it's having slightly lesser size than loadable-components.

My requirement was to have chunk JS/CSS info available at the server only. But it's being available at browser side. So, wanted to reduce the latency. I was able to make chunking perfectly working with loadable-components but couldn't find way to do that part.

@aseem2625 react-loadable have issue tab closed and only merge PR that modifies docs. The project is literally dead. It is not compatible with new features coming into React and not compatible with last versions of webpack. But yeah you could use it if you want.

@neoziro Hi there! Thanks for this package.
I have the same issue. All work fine on client and load chunks correctly. But on ssr I have only (without chunks):

scriptTags

<script id="__LOADABLE_REQUIRED_CHUNKS__" type="application/json">[]</script>
<script async data-chunk="app" src="/build/runtime.js"></script>
<script async data-chunk="app" src="/build/app-chunk.js"></script>

linkTags

<link data-chunk="app" rel="preload" as="script" href="/build/runtime.js">
<link data-chunk="app" rel="preload" as="style" href="/build/app.css">
<link data-chunk="app" rel="preload" as="script" href="/build/app-chunk.js">

styleTags

<link data-chunk="app" rel="stylesheet" href="/build/app.css">

Also, I have two entry points: app for client and node for ssr. SSR result code is correct but doesn`t content js and css links on chunks.
Use it:

const extractor = new ChunkExtractor({ statsFile, entrypoints: ['app'] });
const nodeExtractor = new ChunkExtractor({ statsFile: nodeStatsFile, entrypoints: ['node'] });
const App = nodeExtractor.requireEntrypoint('node');
const rContext = { ...routerContext, ...RC };
const jsx = extractor.collectChunks(
  React.createElement(App, {
    userAgent    : req && req.headers['user-agent'],
    url,
    data,
    routerContext: rContext,
  }),
);
const scriptTags = extractor.getScriptTags();
...

In webpack:

...
externals: [
      '@loadable/component',
      nodeExternals({
        whitelist      : [/\.(?!(?:jsx?|json)$).{1,5}$/i],
        modulesFromFile: true,
      }),
    ],
...

And @theKashey your used-styles work correct. But I don`t know what I can do with js chunks.

You have to use clientStats for the SSR. nodeExtractor(which does not contain css files) is not required at all - try to remove it and repeat.

@theKashey thanks for a quick answer. I use nodeExtractor only to get App component. But without it, I have the same issue.
Where I should search for the problem? On webpack side or in server code?

Server code emits "chunk-name" of the used component and ChunkExtractor them maps those "reported chunks" to the assets, listed in "stats.json"
1 - double check that .css assets are listed in your stats.json
2 - next to the js assets for a specific chunk
3 - chunk names are the same for client and server. Ie chunk names which SSR would report are exists in clientside bundle, so one could load them.

All my chunks has webpackChunkName.
For example, at the start, I render the react-eq-posts-list chunk and both stats.json has it.
Both build folders (ssr and client) has react-eq-posts-list-chunk.js and react-eq-posts-list.css files.

...
    "react-eq-posts-list": [
      "react-eq-posts-list.css",
      "react-eq-posts-list-chunk.js"
    ],
...
"assets": [
...

    {
      "name": "react-eq-posts-list-chunk.js",
      "size": 78703,
      "chunks": [
        "react-eq-posts-list"
      ],
      "chunkNames": [
        "react-eq-posts-list"
      ]
    },
    {
      "name": "react-eq-posts-list.css",
      "size": 8635,
      "chunks": [
        "react-eq-posts-list"
      ],
      "chunkNames": [
        "react-eq-posts-list"
      ]
    },
...
]
...

But I have no one chunk.

🤷‍♂️ I'm afraid it's not solvable without example.
However - try to add some console.log here to understand which chunks were reported to the extractor.

Thanks for the idea.


Result:

[
  'react-eq-shape-overlay',
  'react-eq-menu',
  'react-eq-gallery',
  'react-eq-notifications-notify',
  'react-eq-notifications',
  'react-eq-theme-paper',
  'react-eq-posts-filter-calendar',
  'react-eq-cart-button',
  'react-eq-theme-paper-header-large',
  'react-eq-weather',
  'react-eq-tags',
  'react-eq-ui-feature',
  'react-eq-theme-paper-footer-content',
  'react-eq-theme-paper-auth-info',
  'react-eq-posts-list',
  'react-eq-social-share',
  'react-eq-card',
  'react-eq-special-block',
  'react-eq-comments'
]


usedStyles can find that:

[
  "app.css"
  "react-eq-card.css"
  "react-eq-comments.css"
  "react-eq-events~react-eq-events-form.css"
  "react-eq-gallery.css"
  "react-eq-notifications.css"
  "react-eq-places~react-eq-places-form.css"
  "react-eq-posts-list.css"
  "react-eq-posts-list~react-eq-posts-preview.css"
  "react-eq-posts-list~react-eq-posts-single.css"
  "react-eq-posts-list~react-eq-posts-single~react-eq-posts-theme-form.css"
  "react-eq-posts-preview.css"
  "react-eq-shape-overlay.css"
  "react-eq-shop-routes.css"
  "react-eq-social-share.css"
  "react-eq-tags.css"
  "react-eq-theme-paper-header-default~react-eq-theme-paper-header-large.css"
  "react-eq-theme-paper-header-large.css"
  "react-eq-theme-paper-news-aside.css"
  "react-eq-theme-paper.css"
  "react-eq-ui-feature.css"
]

I will try to understand how this package work and what happens with this.chunks.

I found it!
Just getMainAssets call before addChunk. Because I used it like:


Wrong way

const getExt = (url, { routerContext: RC, ...data }, routerContext, req) => {
  const extractor = new ChunkExtractor({ statsFile, entrypoints: ['app'] });
  const nodeExtractor = new ChunkExtractor({ statsFile: nodeStatsFile, entrypoints: ['node'] });
  const App = nodeExtractor.requireEntrypoint('node');
  const jsx = extractor.collectChunks(React.createElement(App, params));

  const scriptTags = extractor.getScriptTags();
  const linkTags = extractor.getLinkTags();
  const styleTags = extractor.getStyleTags();
  return { jsx, scriptTags, linkTags, styleTags };
};

const renderWithStream = (params) => {
  const { jsx, ...ext } = getExt(params);
  const htmlStream = ReactDOMServer.renderToNodeStream(jsx);
  ...
  return { htmlStream, ...ext };
};

const render = (params) => {
  const { jsx, ...ext } = getExt(params);
  const html = ReactDOMServer.renderToString(jsx);
  ...
  return { html, ...ext };
}

IMPORTANT use renderToString or renderToNodeStream before get scripts and styles with extractor.

const renderStatic = (params) => {
  const { jsx, extractor } = getExt(params);
  const html = ReactDOMServer.renderToString(jsx); // <--- IMPORTANT before use extractor.get
  const scriptTags = extractor.getScriptTags();
  const linkTags = extractor.getLinkTags();
  const styleTags = extractor.getStyleTags();
  return { html, scriptTags, linkTags, styleTags };
}

Can I ask why it is so important? How extractor works with ReactDOMServer?

@aseem2625 maybe it is will be useful for you.

Ok. So renderToNodeStream was the problem? That's understandable - by the time you are asking for getScriptTags - they are not yet rendered. See the docs for the proper setup.

No renderToNodeStream just for example. In my case, I use simple renderToString. Now all right.

Wow i ran into the same issue that is fixed by https://github.com/smooth-code/loadable-components/issues/343#issuecomment-542523420. Never thought that ReactDOMServer.renderToString must be called before pulling the tags 🤯

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zetekla picture zetekla  ·  5Comments

gregberge picture gregberge  ·  7Comments

salzhrani picture salzhrani  ·  3Comments

gcardella picture gcardella  ·  7Comments

valorloff picture valorloff  ·  3Comments