merkle-open / gondel

🚡 Gondel is a tiny library to bootstrap frontend components
https://gondel.js.org
MIT License
36 stars 10 forks source link

Lazy loading of components #68

Closed dhardtke closed 4 years ago

dhardtke commented 4 years ago

Type of issue

The issue refers to:


I think it would be quite useful if Gondel supported lazy loading / lazy initialization, i.e., you'd register a component somewhere globally by providing a factory, e.g.:

import {registerFactories} from '@gondel/core';
const factories = {
    'Button': () => import('Button').then(m => m.ButtonComponent),
    'List': () => import('List').then(m => m.ListComponent)
};
registerFactories(factories);

What I'd imagine to happen would be that Gondel evaluates the given closures as soon as seeing a data-g-name='Button' on the page. Tools like Webpack or Rollup could handle the chunking.

This would be beneficial when a lot of components are placed in the bundle or the components itself would be rather large.

I don't know how much refactoring would need to be done or if this is possible with the current API already (couldn't find this in the docs). Also, some thought will probably have to be put into plugins like hot reloading, etc. Is this even architecturally possible?

What do you guys think?

jantimon commented 4 years ago

Thanks for the idea 👍

We already thought about that before but we learned that Gondel components are usually very small (~20-200 loc) and can be gzipped very well.

But lets assume we would change it to your approach it would change the page loading to the following:

  1. HTML is downloaded
  2. Javascript is downloaded
  3. Javascript is executed
  4. Dom is ready
  5. Search all components
  6. Start download components
  7. Final rendering

during the steps 3, 4 and 5 there is no download going on and this delay will also negatively affect the total load time of your page.

So be careful that your approach might slow down page loads.
However what you propose is already possible!

Gondel supports lazy component registration. So your registerFactories would look like:

import { startComponents, disableAutoStart } from '@gondel/core';

// Don't start components on dom load (as we do it lazy):
disableAutoStart();

// Define the factory mapping
const factories = {
    Button: () => import('Button').then((m) => m.ButtonComponent),
    List: () => import('List').then((m) => m.ListComponent),
};

lazyLoadComponents(factories);

function lazyLoadComponents(factories) {
    // Get a unique list of all components on the current page:
    const componentNames = Array.from(
        new Set(
            Array.from(document.querySelectorAll('data-g-name')).map((component) =>
                component.getAttribute('data-g-name')
            )
        )
    );
    // Lazy load all components in parallel:
    const loadAllComponents = Promise.all(componentNames.map((name) => factories[name] && factories[name]()));
    return loadAllComponents.then(() => startComponents());
}

Another way which might be better is to lazy load huge vendor packages. For example lets lazy load a imaginary datepicker npm package:

import { Component, GondelBaseComponent } from '@gondel/core';

@Component('Button')
export class Button extends GondelBaseComponent {
  async start() {
    const DatePicker = await('date-picker');
    this.picker = new DatePicker(this._ctx);
  }
}

For React widgets we also provide a plugin called @gondel/plugin-react:

import { Component } from "@gondel/core";
import { GondelReactComponent } from "@gondel/plugin-react";

const loader = async () => import("./App");

@Component("GondelReactWidget")
class GondelReactWidget extends GondelReactComponent.create(loader, "ReactApp") {
  setTitle(newTitle: string) {
    this.setState({ title: newTitle });
  }
}

This plugin will lazy load react-dom and ./App to prevent big apps from being loaded for all pages.

Please let me know if any of those approaches helps you.

dhardtke commented 4 years ago

Thanks for your thorough reply!

during the steps 3, 4 and 5 there is no download going on and this delay will also negatively affect the total load time of your page.

You are right, one would have to consider only lazy-loading rarely used components (e.g. those that are located on sub pages and should only be rendered conditionally, e.g. after user interaction).

Gondel supports lazy component registration.

👍 I didn't know that. Thanks for the snippet. For mixed usage (i.e., lazily loading some and eagerly loading other components) we could still use the old way I presume (loading the component in our entry chunk, calling startComponents() should use the previously registered component).

Another way which might be better is to lazy load huge vendor packages.

I saw the example but I was specifically thinking about large components. It could also be quite tedious to always make sure we lazy load all vendor code that we are using. Especially since we could also be using some Webpack magic (loading CSS in JS using different loaders, etc.).

I would definitely opt for the first approach. Maybe even combine it with a MutationObserver so that we notice DOM changes and can start components dynamically.

jantimon commented 4 years ago

I haven't tested the lazyLoadComponents code I wrote above however it is already capable of loading components in a mixed mode. (It is happening in this code snippet: factories[name] && factories[name]()).
So it will only lazy load components found in the factories configuration and once all are loaded start all components.

dhardtke commented 4 years ago

Thanks. I will close the issue as your ideas really help accomplish my goal.