whatwg / loader

Loader Standard
https://whatwg.github.io/loader/
Creative Commons Zero v1.0 Universal
607 stars 45 forks source link

Idea: Add Header to Identify imported names #156

Open Jamesernator opened 7 years ago

Jamesernator commented 7 years ago

For libraries with large collections of functions it can be nice to import them in a single declaration e.g.

import { map, filter } from "./some-itertools.mjs"

// vs individual
import map from "some-itertools/map.mjs"
import filter from "some-itertools/filter.mjs"

However with the former this will fetch the entire library even if a lot of it is not used. Basically what would be nice is if there was an additional header that the server could choose to consider when serving a module e.g.

import { map, filter } from "./some-itertools.mjs"

could send an additional header:

Imported-Names: map, filter

which a smart server could choose to respond with only what's needed instead:

export function map() {
    ...
}

export function filter() {
    ...
}

or even respond more simply with something like:

export { map } from "./some-itertools/map.js"
export { filter } from "./some-itertools/filter.js"
matthewrobb commented 7 years ago

I really like this idea.

rictic commented 7 years ago

Maybe I'm missing something, but how would this handle multiple files that depend on some-itertools that each use different subsets of the file? Would each bespoke version of some-itertools need to be sent down the wire separately? What if they're using partially overlapping subsets of the exports?

My gut feeling is that this use case is better solved by avoiding very large modules that export tons of things.

Jamesernator commented 7 years ago

@rictic I could imagine you could aggregate the names from all modules as you navigate the dependency tree, but I definitely see the concern, especially in the case of circular imports.

However I'd still say being able to have collection modules is desirable rather than having 10 different boilerplate declarations:

import { map } from "rxjs/operator/map"
import { take } from "rxjs/operator/take"
import { merge } from "rxjs/operator/merge"
// ... etc
// and imagine how much boilerplate it would be from a CDN:
import { map } from "https://unpkg.com/rxjs@5.3.0/operator/map.js"
import { take } from "https://unpkg.com/rxjs@5.3.0/operator/take.js"
import { merge } from "https://unpkg.com/rxjs@5.3.0/operator/merge.js"
// ... etc

could become something like:

// And this could still maintain the size-sensitive building
import {
    map,
    take,
    merge,
    // ...etc
} from "rxjs/operator" // or something like that

// Or from a CDN
import {
    map,
    take,
    merge,
    // ...etc
} from "https://unpkg.com/rxjs@5.3.0/operator" // or something like that

My reasoning for suggesting something of the form (even if it can't work as I suggested) for the default loader is:

  1. Size-sensitive building is more a concern of internet applications given that page load time can still be a significant factor (especially on mobile) so I thought it would fit better as part of the loader instead of part of JavaScript itself
  2. I wanted it to be transparent on the consumer side of the library especially if they get their data from a CDN, that way if a CDN were to implement it then people could automatically get the benefits for free which is why I didn't want to to simply be part of a custom loader (otherwise every single client who wants size reduction of this form would have to include virtually identical loaders)

I'm very open to suggestions to other methods reducing boilerplate in these sorts of declarations, obviously something like this could improve things but it'd still be a pain:

// Just using import with for the sake of exposition
import with {
    // Some special form of block which is run before normal module execution time
    const a = operator => `https://unpkg.com/rxjs@5.3.0/operator/${ operator }.js`
    // Allow expressions after import within such a block as it's run at a different time
    import { map } from a('map')
    import { take } from a('take')
}
Jamesernator commented 7 years ago

An alternative I thought of that would be a language level change could be a variant of the export-from statement that tells the engine that if they're not imported then they need not be resolved fetched or evaluated:

// Using static here for exposition to say that these should be loaded *only* if requested
export static { map } from "./operators/map.js"
export static { filter } from "./operators/filter.js"
export static { reduce } from "./operators/reduce.js"
// ...etc

// In another file
import { map, filter } from "rxjs/operators"
// reduce, etc need not be loaded as they were declared to be only fetched/evaluated if actually required.

One downside of this approach (not that they're mutually exclusive) is that you'd still have to send a full file of export static declarations instead of just the ones you need, however this feels fairly acceptable as a declarations file isn't likely to be large anyway.

On the upside is that it would simply work for both consumers and CDNs (whereas with my previous suggestion a CDN would have to be aware of how to serve each module, which might need be defined by metadata in the module). Whereas this approach fully shifts the work to the module developer (who would only need to add a few export-static-from declarations, and in the worst case where the module developer doesn't do this then the module simply continues to work albeit at a higher bandwidth/load time for consumers.