Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to lazy load(code splitting) stories(and respective component source)? #6885

Open
ganapativs opened this issue May 27, 2019 · 26 comments
Open

Comments

@ganapativs
Copy link

Is your feature request related to a problem? Please describe.
We have many components(100+) and we want to write stories for all of them, eventually replacing component development with storybook.

The bundle size is growing enormously as we are writing stories for new components. This is affecting initial page load time as all the component JS & stories are loaded on page load.

Describe the solution you'd like
We would like to lazy load each story file(which imports component). so that, only JS required to render current stories are loaded. and subsequent navigation to other stories loads additional JS.

Describe alternatives you've considered

  • Tried dynamically importing each component using react-loadable, but in this case the propTypes/defaultProps/smart-knobs are not read.
  • Creating DLL using webpack and injecting it probably(Haven't tried yet, but this would solve the problem partially)

Are you able to assist bring the feature to reality?
May be! Not sure.

Additional context
none

@ganapativs ganapativs changed the title How to lazy load(code splitting) stories and respective components? How to lazy load(code splitting) stories(and respective component source)? May 27, 2019
@github-actions
Copy link
Contributor

github-actions bot commented May 28, 2019

Automention: Hey @igor-dv, you've been tagged! Can you give a hand here?

@ganapativs
Copy link
Author

Am I using the storybook in a wrong way?

Found some related issues(#696 & #713) though. but, none of them solves the issue.

It would be nice if we can support this feature out of the box in storybook. Also, if someone has already tried some implementation related to this, please share!

@cong-min
Copy link

I also need dynamic imports.

I tried to do this, but the display is blank :

storiesOf('Test', module)
.add('Dynamic Import', () => import('./something.js'));

I suggest storyFn should support await Promise

@smurrayatwork
Copy link

smurrayatwork commented Jun 13, 2019

@ganapativs @mcc108,
So I've got this working myself using React lazy and Suspense! This requires your components to be default exports and not named, as React lazy only works with default exports for now. We happen to be using the addon-info addon, so you can ignore the { info: DividerDocs } part below if you're not using it:

import React, { lazy, Suspense } from 'react';
import { storiesOf } from '@storybook/react';

import DividerDocs from './Divider.md';

storiesOf('atoms', module)
  .add(
    'Divider', () => {
      // The Divider component is located in index.js and is the default export.
      const Divider = lazy(() => import('./index'));
      return(
        <Suspense fallback={<div>Loading...</div>}>
          <Divider />
        </Suspense>
      );
    },
    { info: DividerDocs }
  );

EDIT: This doesn't lazy load the story itself but just the component(s) the story is about. I think you can't lazily load all story files right now (such as in a config.js file) because certain things in storybook appear to need all stories to be added with require().

If you use this, you can get defaultProps/propTypes from the _result property on the lazily loaded component (in this example Divider._result.defaultProps. If that doesn't work, you could also make a new file, import your component in that file, then export defaultProps/propTypes, which can then be imported into your story file:

import Divider from './index';
const { defaultProps } = Divider;
export defaultProps;

@shilman
Copy link
Member

shilman commented Jun 13, 2019

@tmeasday @ndelangen possibly a good argument for keeping stories as functions? ☝️ ☝️ ☝️

@tmeasday
Copy link
Member

@shilman definitely!

@ndelangen
Copy link
Member

I had a PR adding support for returning a promise from a storyFn about a year ago.

But back then no-one seemed to want it.

I'll see if I can make it happen again.

@ganapativs
Copy link
Author

@smurrayatwork Yup, Solution works well for lazy loading component. But, I couldn't get addon-info or addon-smart-knobs to work with it.

Component._result is a Symbol and is empty at first. it's updated to Component._result.defaultProps once the component lazy loads.

The above addons doesn't detect the changes. hence, no propTables or smart knobs. Need to investigate this further 😇

@ganapativs
Copy link
Author

ganapativs commented Nov 11, 2019

The official storybook demo has many components and fetches around 8.7MB of uncompressed JavaScript(???).

Lazy loading stories would help in this case. it will also improve the page load performance significantly.

Let's make this happen! 😬

Update(Feb 2020):

@KaboomFox
Copy link

I'm curious how to best do this in storybook 5.3 with the main file now. it would be cool to wrap files from main.js

@ganapativs
Copy link
Author

It would be nice if someone from the storybook team can help fix this issue on priority. It's been a long time since the issue was opened. I can spend some time on it if needed.

As mentioned earlier, we have many components(150+). We want to speed up the dev process and make components discoverable using the storybook. This feature is crucial for us to use the storybook efficiently 😅

@ndelangen
Copy link
Member

@ganapativs I'd love to help you work on this, are you available for a zoom call in the foreseeable future?

https://calendly.com/ndelangen/storybook

@ganapativs
Copy link
Author

@ndelangen Scheduled. Thanks for your time.

@drayalliance-johnpittman

This would make everyone's day guys. I initial open an issue for this back for version 1 because we immediately had to ditch storybook because of the load times. My new company is now going to use v5 and i am so surprised it's still not in. With webpack already doing the heavy lifting i do not understand the complications. Keep fighting the good fight!

@smurrayatwork
Copy link

smurrayatwork commented Apr 22, 2020

Here's an updated example using CSF format:

import React, { lazy, Suspense } from 'react';

import { withKnobs, text, select } from '@storybook/addon-knobs';

import ArrowButtonDocs from './ArrowButton.md';
const ArrowButton = lazy(() => import('./index'));

export default {
  title: 'atoms/buttons/ArrowButton',
  component: ArrowButton,
  decorators: [withKnobs],
  parameters: {
    knobs: {
      escapeHTML: false
    },
    info: {
      text: ArrowButtonDocs
    }
  }
};

export const Default = () => {
  const props = {
    direction: select('direction', ['left', 'right']),
    href: text('href', ''),
    info: text('info', 'Left'),
  };
  return(
    <Suspense fallback={<>Loading...</>}>
      <ArrowButton {...props} />
    </Suspense>
  );
};

@ganapativs
Copy link
Author

Sadly I cannot spend time on this right now 😕 Will come back to this sometime later.

@smurrayatwork
Copy link

What about something like this for dynamic story loading:

// .storybook/preview.js
import { configure } from '@storybook/react';

const loaderFn = () => [
      require.context('../src/components', true, /\.stories\.js$/, 'lazy'),
];

configure(loaderFn, module);

The important part here is the last parameter to require.context, mode, which allows you to set things to lazily load or other options. It defaults to sync. See the following for more info.

@tmeasday
Copy link
Member

tmeasday commented May 1, 2020

Hey everyone, we'd love to see proper code-splitting in Storybook, we've all got big bundles too and it surely isn't ideal! I know this is one important way Storybook could improve for application development and its definitely something we've been thinking about a fair bit.

Let me outline some of the difficulties and ideas/progress towards solutions we've had so folks know what's up and how they can possibly help.

There are two basic ways you could code split in Storybook, both of which have been discussed on this ticket: manual (story-level) and automatic (component-level).

Manual code splitting.

As suggested above by @mcc108, if a story is allowed to be an async function, you use dynamic import()s in the story function:

export const myStory = async () => {
  const MyComponent = import('./MyComponent');
  return <MyComponent x={y} />
}

There's no fundamental reason SB doesn't let you do that. In fact we'd like to fully support this in Storybook 6.0 -- we have a open ticket about it, and a PR adding support to the html framework.

In fact we kind of need it for the server framework coming in 6.0!

There are probably some kinks with common addons that need to be worked out, and support will need to be framework by framework but if folks pitch in for their favourite framework I think we can make it happen. (NOTE: there are some complexities for React specifically -- see this comment -- if people have thoughts about that, perhaps reply on that thread).

HOWEVER, there are some serious downsides in using async stories for code splitting:

  1. It's manual, which is a pain.
  2. We don't know about the component for a story until it renders which makes doing stuff like Storybook docs much harder. In theory we could probably make it work (I think?) but it won't right now. We probably won't invest in making this happen because of 1.

In short, I really thing async stories is more about other use-cases like loading data/et al. in order to render a story, which is a power tool that should be used with care (can talk more about that elsewhere).

Automatic code splitting

As suggested above by @smurrayatwork, we could use the new stories field of main.js (introduced in 5.3) to generate a lazy require.context (or probably a bewildering array of other webpack techniques to achieve the same effect). However, the details of exactly how the code-split works isn't the issue.

The challenge is that if you don't load all the story files right away, how do you display a list of stories in the sidebar? It's a bit of a chicken and egg problem.

The answer we are moving towards is generating a list of stories (stories.json) at build time. In fact this is something that @ndelangen has already been exploring for the composition feature (coming in 6.0) and it is something that is enabled by the static-exporting CSF feature we shipped back in 5.2 (with one eye on this problem!) [1]

So in short I suspect this is something that is not too far away from being realised although it will require some effort to get it over the line. I am hopeful the building blocks are close to being in place. But we are looking forward to finding what zany things folks are doing in their stories files that breaks stories.json as part of the 6.0 release.

PS -- the composition also allows you to break your Storybook up into smaller, more manageable parts and compile them separately, but still load them into a single UI. It's not exactly a perfect solution (it's not why we build it) but it could go a long way towards helping with the problem. Perhaps @ndelangen can post a demonstration somewhere.

[1] It's not really possible to get a list of stories at build time if folks are using storiesOf()

@andreiQuant
Copy link

Maybe it is not an acceptable solution for everybody but it works for me :)

import React from "react";
import { useEffect, useState } from "react";

const TestWidget = () => <div>It works!</div>;

export default {
  title: "Test Widget",
  component: TestWidget,
  decorators: []
};

const Wrapper = ({ promise }) => {
  const [component, setComponent] = useState();

  useEffect(() => {
    promise.then(setComponent);
  }, []);

  return !component ? <div>Loading...</div> : component;
};

export const index = () => {
  const promise = new Promise(resolve => {
    setTimeout(() => {
      resolve(<TestWidget />);
    }, 2000);
  });
  return <Wrapper promise={promise} />;
};

@imanderson
Copy link

imanderson commented Jul 24, 2020

@andreiQuant thanks for this, I have been looking for quite a while now.

It seems to work, I wrapped it on a decorator:

withPromise.js

import React, { useEffect, useState } from 'react'

const withPromise = promise => story => {
  const [component, setComponent] = useState()

  useEffect(() => {
    promise.then(data => setComponent(story(data)))
  }, [])

  return !component ? <div>Loading...</div> : component
}

export default withPromise

and then you just need to define your story like this:

Example.stories.jsx

import React from 'react'
import ExampleComponent from './index'
import ExamplePromise from './ExamplePromise'
import withPromise from './withPromise'

const promise = new Promise(resolve => {
  ExamplePromise.then(data => resolve(data))
})

export default {
  component: ExampleComponent,
  title: 'Example',
  decorators: [withPromise(promise)]
}

export const index = (data) => <ExampleComponent data={data} />

@andreiQuant
Copy link

@imanderson
It looks even better now!
One could parameterize the "loading state" for better customization but other then that it's perfect :D

import React, { useEffect, useState } from 'react'

const withPromise = {promise, loading} => story => {
  const [component, setComponent] = useState()
 const placeholder = loading ? loading : <div>Loading...</div>;

  useEffect(() => {
    promise.then(data => setComponent(story(data)))
  }, [])

  return !component  ? placeholder : component
}

export default withPromise

@imanderson
Copy link

@andreiQuant perfect! 👌🏼

@shilman
Copy link
Member

shilman commented Oct 31, 2020

FYI, we've released async loaders in 6.1, which can be used as a building block for this kind of thing: #12699

Please give it a try in the latest alpha:

npx sb@next upgrade --prerelease

@Christopher-Hayes
Copy link

Christopher-Hayes commented Feb 1, 2022

Btw, for anyone coming across this now. You no longer need to jump to alpha using the code above. That is pre-v6.1
The latest Storybook release has async loaders support.

Updating Storybook to the latest release should work now:

npx sb@latest upgrade

PS: I'm using code-splitting now on a multi-site component system and the speed up is incredible.

@tmeasday
Copy link
Member

tmeasday commented Feb 2, 2022

FTR Storybook also code splits on CSF files if you opt into the storyStoreV7 feature in 6.4: https://storybook.js.org/blog/storybook-on-demand-architecture/

@sibelius
Copy link

it is breaking for us #14405

@shilman shilman removed the todo label Jun 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests