More about Dynamic Imports, A Few Questions

First and foremost thank you to MDG for making advances with Meteor.

Question 1

  • Destructuring is off the table when using the import() function correct?
    E.g. import { subconscious } from 'brain';

Question 2

  • In @benjamn youtube video in regards to dynamic imports, refactors some code to be dynamically imported, but only through one file. In the scenario where I dynamically import react in my client/main.html and import my imports/ui/App.jsx. What happens to my regular import React from 'react' that is in my imports/ui/App.jsx. I’m wondering if once you dynamically import a lib, does it also need to be dynamically imported throughout the app.

Example

I just wanted to show an example of the current state of my client/main.js;

It seems very redundant, and I rely on the ordering of the items my promise all array. I’m sure this can be remedied by using async and await.

Another thing to note; is that I’m destructuring twice on some occasions, because I don’t need the default of the module, I need another property.

Promise.all([
  import('react'),
  import('meteor/meteor'),
  import('react-dom'),
  import('../imports/ui/App'),
  import('apollo-client'),
  import('meteor/apollo'),
  import('react-apollo'),
])
  .then(modules => {
    const [
      React,
      meteor,
      reactDom,
      app,
      apolloClient,
      meteorApollo,
      reactApollo,
    ] = modules;
    const { Meteor } = meteor;
    const { render } = reactDom;
    const { ApolloClient } = apolloClient;
    const { meteorClientConfig } = meteorApollo;
    const { ApolloProvider } = reactApollo;
    const App = app.default;
    const client = new ApolloClient(meteorClientConfig);

    Meteor.startup(() => render(
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>,
      document.getElementById('app'))
    );
  });

As a side note, dynamic imports reminds is similar to how one would import WASM file.

fetch('simple.wasm').then(response => response.arrayBuffer())
.then(bytes => instantiate(bytes, importObject))
.then(instance => instance.exports.e());
// source: http://webassembly.org/getting-started/js-api/
1 Like

These are great questions!

Answer 1

Destructuring does work with import(…), though you have to take the Promise API into consideration. This means you can either destructure the parameters of your .then callback:

import("brain").then(({ subconscious }) => {
  console.log(subsconscious);
});

or destructure the result of an await expression in an async function:

async function foo() {
  const { subsconscious } = await import("brain");
  console.log(subsconscious);
}

Answer 2

If you are dynamically importing imports/ui/App.jsx, then any non-dynamic (static) import declarations in that module will also be fetched dynamically. In other words, you don’t have to use dynamic import(…) everywhere, just when importing the root modules of your dependency tree.

The mistake to avoid here is accidentally using a static import React from "react" declaration in a module that is eagerly evaluated when your apps starts up, because then you lose the benefit of dynamic import(…).

To verify that you’ve removed react from your initial bundle, try calling require("react") in your browser JS console (not in your application code). The require should fail if react is only available dynamically. If it succeeds, then you must still have a static import or require for react somewhere in your non-dynamic dependency tree.

Example

As you mentioned, your example code can be a little cleaner if you use async functions and await expressions:

async function main() {
  const [
    React,
    { Meteor },
    { render },
    { default: App },
    { ApolloClient },
    { meteorClientConfig },
    { ApolloProvider },
  ] = await Promise.all([
    import('react'),
    import('meteor/meteor'),
    import('react-dom'),
    import('../imports/ui/App'),
    import('apollo-client'),
    import('meteor/apollo'),
    import('react-apollo'),
  ]);

  const client = new ApolloClient(meteorClientConfig);

  // ...
}
7 Likes

@benjamn, thank you very much for taking the time and addressing my questions. Greatly appreciate it.

One other observation is testing this locally, in your video you display local development but then switch to Galaxy to make better case of dynamic imports.

I’ve ran Meteor with the production flag, but notice the app flash (slight delay) when refreshing (loading), is this normal? I thought it would be faster after first load since; it loads from cache, and would avoid any flickering, but perhaps local production build is not that.

@benjamn, in regards to Answer 2, I did the following, and can still require(‘react’) from the console. This is in meteor local mode, and meteor run --production

  1. meteor create --bare projectName
  2. meteor npm install --save react react-dom

1 Like

Hi @benjamn - Answer 2 doesn’t seem to be the way it works in my testing (I’ll try to do a reduction soon).

No matter what I do, things that should be loaded only after my routes seem to end up in my initial bundle. In one of my apps my bundle file is 307KB (after gzip) both before and after refactoring my routes with react-loadable, and my bundle size as reported by bundle-visualizer is also exactly the same.

I still have to make sure this isn’t some problem in react-loadable, but given the setup, it wouldn’t seem to be.

It would also be helpful if there was some output when additional modules are loaded - tracing something to the console, or to the network tab when modules are loaded over DDP, or even meteor dev tools. Or if there is some way to build a graph of which modules are causing which modules to load or be bundled. This whole system is very opaque, I’m just not sure how to analyze any of this. I’m fairly certain I’ve done the dynamic import setup correctly, but I don’t know which tools to use to validate it all.

I’m also not certain how to read [bundle-hash].stats.json - does this include every module, or only the ones included in the initial bundle? (it would seem to include both - but as I’ve said, my initial bundle seems to include everything.)

1 Like

@captainn I agree I’ll need a reproduction to help you debug this. Are the modules you expect to be dynamic in an imports (or node_modules) directory, at least?

The .stats.json file includes information for every module, both those that are statically bundled and those that are dynamically available. In the case of static modules, the sizes reflect the actual code of the modules. In the case of dynamic modules, the sizes merely reflect a list of each module’s dependencies, which allows the client to traverse the dependency graph, even though it does not have the code for dynamic modules. Since the string identifiers imported by a module are all substrings of the code of the module, this manifest of dependencies is always (considerably) smaller than the original module.

I realize it may be surprising that any information about dynamic modules exists on the client until the modules are fetched from the server, but this dependency information is critical to enable the client to make a single request for all missing dynamic modules. If the client knew nothing about the dependencies of dynamic modules, it would have to rely on the server to resolve those dependencies, but then the server would have to keep track of which modules every client already had, which breaks down in a number of ways that I explored in the early development of Meteor 1.5. In short, the client would either need to communicate its requested/received module set to the server on every dynamic request (which ends up being much more data over time than the dependency manifest), or the server would have to be stateful (which is a problem for scalability, and/or fetching modules from a CDN).

@benjamn I agree with @captainn, I feel like I’m doing things correctly but have trouble verifying that it’s actually getting done correctly. Outside if building a bundle and inspecting it bundle/programs/web.browser/dynamic/node_modules

Is this because none of this matters through local development, and only visible through production?

In the example above I tried to follow your recommendation.

To verify that you’ve removed react from your initial bundle, try calling require(“react”) in your browser JS console (not in your application code). The require should fail if react is only available dynamically. If it succeeds, then you must still have a static import or require for react somewhere in your non-dynamic dependency tree.

But even after starting a new project, when ever I require('react'), the console did load that module. I feel like I’m missing something to understanding this.

I can confirm that my dynamic modules are not available on the first screen through the console with require.

I have also been able to observe a change in the bundle size, though it’s minuscule, only maybe 13KB. But that’s something! It probably indicates that I am still linking in large libs somewhere, without intending to - one of them comes from a local package - I’ll focus on moving that into the rest of the code - actually, how can I set up the local meteor package to support dynamic-import?

1 Like

What techniques and patterns are you using to analyze what’s going on? I tired using a dynamic module on first load and it hammered my application to dust. I mean my application was kinda quick on initial load before, but when I tried to dynamically import load the first stuff, it was getting choked out or something. The application is so big now, so many libs, it was hard to say what was causing the slowdown.

My primary tool is subjectivity. Some better tools would be great though

About this - if you try to import one module from a package but not another, does dynamic-imports still import the entire package?

I have package that exports a big object and a small object. I try to import the small object into the code right away, and the bigger one later. However, I’m noticing that when I import the small one into the client bundle, the big one gets added too.


Here is the main module of a package. It is set as { lazy: true }.

// First, import client-only code 

import { security as clientSecurity } from '../imports/client/security'
import { defaults } from '../imports/client/defaults'

var client = {}
client.security = clientSecurity;
client.default = defaults;

// Second, import shared code

import { actions } from '../imports/shared/account/actions'
import { security } from '../imports/shared/security';
import { functions } from '../imports/shared/functions';

var shared = {};
shared.account = {};
shared.account.actions = actions;
shared.security = security;
shared.functions = functions;

// Finally, assemble and export

var Config = {}
Config.client = client;
Config.shared = shared;

var chicken = "bbq"

export { Config, chicken }

If chicken ever gets imported, then both chicken and Config get added to the client bundle.

1 Like