simplajs / simpla

Open, modular, and serverless content management for a modern web
https://www.simplajs.org
MIT License
527 stars 36 forks source link

Add prefetch method #65

Closed madeleineostoja closed 6 years ago

madeleineostoja commented 7 years ago

Add a method to Simpla that mirrors what <link rel="prefetch" href="..."> does (ie: fetch resources you will likely need in future, lazily in idle cycle), but for our JSON data. This would be really useful for optimising performance of dynamic collections of content: eg - documentation on Simpla.io (where the idea came from).

Implementation would just be (very) light sugar around requestIdleCallback:

function prefetch(resource) {
  if (!window.requestIdleCallback) {
    return;
  }

  requestIdleCallback(() => {
    switch (typeof resource) {
    case 'string':
      Simpla.get(resource);
      break;
    case 'object':
      Simpla.query(resource);
      break
    default: 
      throw new Error('Invalid resource, can only prefetch paths or query objects');
    }
  }
}

Then future gets and queries (which the user would actually use/observe) would just read straight from buffer and be instant.

Would only work in Chrome and Firefox currently (http://caniuse.com/#feat=requestidlecallback), but it's progressive enhancement so I don't think that matters. Just document it as such. There is a shim with setTimeout, but I it defeats the purpose of this (people can just call Simpla.get in advance if they don't care about idle cycles), should this method just be pure prog enhancements for browsers that can handle it, like prefetch is.

madeleineostoja commented 7 years ago

Question: since this is just such light sugar around requestIdleCallback should we document it as a pattern, rather than bundle it into a method? I still like it as a method, because it'll encourage people to use it more. requsetIdleCallback is still reasonably unknown/unused. Can always remove this later if need be.

bedeoverend commented 7 years ago

I like the idea, but yeah given it's so light maybe we should just use as a pattern, and if it becomes popular enough / valuable enough, add it into the core?

madeleineostoja commented 7 years ago

This works a treat on simpla.io's docs, and I think it should definitely be a method on Simpla, because we can sugar it into a proper prefetching tool for Simpla data.

You wouldn't want to prefetch an actual query, and prefetching an individual item has pretty limited utility, instead you'd want to prefetch a whole section of a path. So we should make prefetch a method that just takes a path, and hydrates all items in that path down to its leaf nodes, perhaps with an optional depth flag in future.

// Hydrates all articles and their content on simpla.io docs
Simpla.prefetch('/docs/guides');

// Maybe in future
Simpla.prefetch('/docs/guides', { depth: 2 });

Example use-cases for this, where just the requestIdleCallback pattern isn't good enough:

Providing a depth param to queries I think is the wrong solution to this, because then you get back a pretty messy result which I can't think of a use-case for in actual queries (as opposed to hydrating the buffer). It's also weird thinking in terms of queries when prefetching data.

Ideally prefetch would make individual requests for every item in the path so it can multiplex them over http/2, but for initial release a monolithic fetch for the whole JSON tree is fine.

madeleineostoja commented 7 years ago

Or, if we wanted to make this more low level, we could create a hydrate method that does what I describe above (not returning data, just fetching and hydrating buffer - or likely returning a simple Boolean promise when its done), but without requestIdleCallback. Then users can just wrap hydrate in requestIdleCallback themselves. Can't think of many use-cases for that off the top of my head, only plus is the basic tool would work in all browsers, but the prog enhancement is more steps.

Intuitively I'd say bundle it all into a dedicated prefetch method.

bedeoverend commented 7 years ago

Re: the depth thing. Yeah I agree, not quite right - I think a good alternative might be ancestor vs parent. So a prefetch will run an "ancestor" query, so like Simpla.query({ ancestor: '/some/path' }) which would fetch all items of any depth below that path.

As for hydrate - I quite like that one. I think in a lot of scenarios where you want to use the get + observe pattern, using observe + hydrate would solve the issues there. You could have an option as to whether to make hydration lazy or eager which would just mean it would run in a requestIdleCallback. Don't see a problem with it being bundled with prefetch either - IMO they could easily be the same method, just with an options block to configure how they behave. I think they boil down to the same idea: fetch data for later usage, it's just configuring how much data (depth) and how important the data is (lazy).

madeleineostoja commented 7 years ago

I'd be against shoehorning this functionality into query, because they're entirely different use-cases. I can't think of a reason you would want to query and operate deeply on all the children of a path, and if you do it's probably for some kind of administration purpose, in which case you can use the REST interface directly. Even if a use-case for this does arise in future, I still think they should be different tools, even if they do similar things.

I'd also like to make this it's own thing because if it doesn't rely on anything other than making a plain request to a given URL (endpoint/projectID/path), then in future we can optimise the SDK to be ready to prefetch before anything else, which would help perf and prefetching page content while the document is still parsing.

On the second suggestion, that's actually along the lines of what I came back here to suggest myself:

Simpla.prefetch('/home');  // Requests all content below /home when idle, if available
Simpla.prefetch('/home', { force: true }); // Requests all content below /home no matter what

The first one uses requestIdleCallback and is useful for prog enhancement like Simpla's docs, the second one just makes the request(s) and moves on, useful for cases where that content will definitely be needed and you want it eager-loaded, eg: in the head of a small static page.

If this is as straightforward to implement as I think it is, I think we should do it soon and dogfood on simpla.io, to explore patterns of how this could be used more deeply to make Simpla performant by default. By far the biggest bottleneck right now is request latency, which largely comes from the fact that content isn't fetched until the document is already fully parsed and loaded, and elements are upgraded. This could be mitigated by fetching the content a page needs in the <head>, so it's (more likely to be) ready for Simpla elements as soon as they upgrade. Because we have this awesome data buffer totally independent of elements and the DOM, and we're not taking advantage of it.

Ironically this is actually hard to do on simpla.io itself, because of how we do routing with inert templates, but point still stands.

madeleineostoja commented 7 years ago

Actually it should be the other way around

Simpla.prefetch('/home'); // Prefetches /home and all child paths
Simpla.prefetch('/home', { idleOnly: true }); // Prefetches /home and all child paths in idle cycles if supported

And tbh we don't really need to bundle that second feature, at least not inititally. Can just document the pattern

if (window.requestIdleCallback) {
  requestIdleCallback(() => {
    Simpla.prefetch(path);
  });
}

And if it's a big use-case, bundle it along with the setTimeout shim into a idleOnly option or something to prefetch. Also means prefetch isn't flaky by default w.r.t browser support.

madeleineostoja commented 7 years ago

@bedeoverend what would a spike on a plain prefetch (ie: hydrate given path) look like? Would be keen to dogfood on simpla.io and in collections.

bedeoverend commented 7 years ago

@seaneking I'd be keen to look into caching parent queries on the server first, as without that, this wouldn't be that great - particularly for simpla-collection.

As for a spike itself, not sure, I'll try spend some time on this early next week and let you know how it's looking.

madeleineostoja commented 7 years ago

simpla-collection doesn't use queries? Neither will blog elements, most likely.

And awesome, let me know when we've got something and I've got a bunch of low-hanging-fruit to try it on. Super keen to try it.

bedeoverend commented 7 years ago

So prefetch will need to use queries to fetch all the children of a given path. Doesn't matter how collection / blog elements etc. are implemented. I meant it'd be useless for collection because collection would call prefetch, but that would be a slow query, while fetching the data at collection and then fetching content as it's stamped would all be through a CDN, so would probably end up faster anyway.

madeleineostoja commented 7 years ago

Oh right, I figured this would be a different implementation, just doing a fat JSON dump of all the data in a path using fetch or whatever. Hmm okay, fair enough. Damn.

madeleineostoja commented 7 years ago

In staging, blocked by #53

madeleineostoja commented 6 years ago

This will now need to be rewritten for when we introduce indexes in v3 - @bedeoverend is it even easily doable anymore? You'd need to create indexes for every ancestor you want to prefetch right? Probably not feasible.

bedeoverend commented 6 years ago

@seaneking instead of prefetching an ancestor, you'd want to prefetch an index. Might be worth closing this / pausing it until we have a solid understanding of how indexes will work / look.

madeleineostoja commented 6 years ago

Yeah and that is of pretty limited use. I mean, you could just fetch an index instead if you have already defined it. The beauty of the prefetch method was that you could call it on page change (for example) to start eager loading in all the content while the DOM parsed. You're not going to have indexes for every page of content on your site.

Closing.