Add `document.ready` promise #127

Open
tabatkins opened this Issue Sep 10, 2015 · 57 comments

Projects

None yet
@tabatkins
Collaborator

It's annoying to deal with window.onload, since your async code might not know whether it's run before or after the load event has fired. jQuery makes this easy with its $.ready() function, which functions in a promise-like manner.

So anyway, let's add a .ready promise to Document.

@annevk
Member
annevk commented Sep 10, 2015

So what would the equivalent be for DOMContentLoaded?

@foolip
Member
foolip commented Sep 10, 2015

Is there any implementor interest, and have you done a search in existing content to see if many scripts already use document.ready in a way that could break?

@annevk
Member
annevk commented Sep 10, 2015

There is definitely interest for this. Using promises for state transitions is much more developer friendly.

@jakearchibald
Contributor

Here's a discussion on this from last year https://lists.w3.org/Archives/Public/public-whatwg-archive/2014Mar/0091.html

Completely agree. Anything with an onload/error event should get a .ready promise property. Document, images, script, link etc.

@jakearchibald
Contributor

(But I do think .ready should be dom ready)

@foolip
Member
foolip commented Sep 10, 2015

(But I do think .ready should be dom ready)

Do you mean that document.ready should correspond to DOMContentLoaded? It looks like that's the jQuery defintion, so I agree, making document.ready mean anything else seems likely to create confusion.

@jakearchibald
Contributor

Yep, agree for the same reasons.

We might want .loaded for full load, but we should think about what that could mean for .ready on other APIs.

@foolip
Member
foolip commented Sep 10, 2015

Yeah, it would bad if we ended up with some incomprehensible mix of .loaded and .ready, and it looks like at least FontFaceSet.ready already means "ready to use" which is the same I guess we'd want for HTMLImageElement.ready... I guess .ready everywhere except for the document (and window?) load event?

@fvsch
fvsch commented Sep 10, 2015

A suggestion:

  • window.ready -> Promise for window.onload
  • document.ready -> Promise for DOMContentLoaded

Because the window is ready when everything is loaded, but the document is ready when the initial tree was fully built or something?

One possible downside is that there are a lot of jQuery exemples out there with $(window).ready() and $(document).ready() used indiscriminately. So this precedent could create confusion. (Though technically one can do jQuery().ready() to get the same prototype method.)

@domenic
Member
domenic commented Sep 10, 2015

In general I feel that .loaded is what we would use most of the time, with a special jQuery-inspired case of .ready for the DOMContentLoaded counterpart.

@jakearchibald
Contributor

That fits with onload. Makes sense.

@foolip
Member
foolip commented Sep 11, 2015

We already have FontFaceSet.ready, so it's a bit late to make .ready the special case.

@jakearchibald
Contributor

There's also navigator.serviceWorker.ready - but that doesn't just mean loaded, so it's a special case already

@zcorpan
Member
zcorpan commented Sep 14, 2015

Images are also special.

https://html.spec.whatwg.org/multipage/embedded-content.html#img-available

Images can also have two requests going at the same time. The "pending" request is basically hidden currently, but there is some interest in exposing it. See ResponsiveImagesCG/picture-element#269

strawman proposal:

var currentURL = img.currentURL;
var currentPromise = img.ready;
var pendingURL = img.pending.currentURL;
var pendingPromise = img.pending.ready;
@annevk
Member
annevk commented Sep 14, 2015

@zcorpan do we need a distinct object or is simply using pendingCurrentURL and pendingReady sufficient? Extra objects complicate the lifecycle quite a bit.

@zcorpan
Member
zcorpan commented Sep 14, 2015

No particular need for a distinct object.

@annevk
Member
annevk commented Sep 14, 2015

@zcorpan does that cover everything <picture> provides, too?

@zcorpan
Member
zcorpan commented Sep 14, 2015

@annevk Yes.

@domenic
Member
domenic commented Sep 14, 2015

I'm a little stuck on ready vs. loaded now :(. The precedent of jQuery is ready = DOMContentLoaded. The precedent of FontFaceSet is ready = loadingdone (~= load). Is it possible for us to have a consistent story?

Maybe one consistent story would be that suggested by @fvsch in #127 (comment): document.ready is DOMContentLoaded; window.ready is loaded. Then we use ready everywhere corresponding to load, with document being the special case where it corresponds to DOMContentLoaded.

We could also omit or rename window.ready if we think having it differ from document.ready is too weird.

What do people think?

@domenic
Member
domenic commented Sep 14, 2015

Another consistent story would be ".ready means just do the right thing", and we say that that's DOMContentLoaded for documents (and windows?). That also jives with navigator.serviceWorker.ready not meaning load.

@tabatkins
Collaborator

FontFace and FontFaceSet don't have any other comparable notion of "ready". I'm fine with it meaning "do the right thing" (tho of course we have to police new usages to make sure it really is the right thing).

@arturparkhisenko

👍 we all need it for ... web-components revolution 😈

@foolip
Member
foolip commented Sep 15, 2015

Yeah, the policy ".ready means just do the right thing" seems reasonable. If we could avoid a mix of .loaded and .ready that would be nice, but that really implies that window.ready has to correspond to the load event while document.ready corresponds to the DOMContentLoaded event. That seems OK to me right now...

@annevk
Member
annevk commented Sep 15, 2015

window/document.ready() seems bad given that onload means the same on both of them. Perhaps document.subresourcesReady or document.dependenciesReady or some such.

@annevk
Member
annevk commented Sep 15, 2015

"The right thing" does seem a tad hacky though and will likely change over time as more of the kernel gets exposed and programming patterns evolve.

@jakearchibald
Contributor

document.ready resolving on DOMContentLoaded seems like the only sensible choice given how jQuery and other libraries treat "document ready".

I still like @domenic's idea of .loaded being the less hand-wavey "This thing and all its possible related resources have finished loading", whereas .ready is reserved for some thing-specific notion of readiness. It means the font API is using the hand-wavey thing rather than the most specific thing, but it's not wrong.

Agree with @annevk on window.ready vs document.ready. The promise property name should reflect the event name if one already exists, so .loaded matches load, if we promisified IDBRequest it'd be .success. DOMContentLoaded is a looong event name for something so common, and not all that descriptive for it, so .domReady or .ready is a better fit

@domenic
Member
domenic commented Sep 15, 2015

@foolip

If we could avoid a mix of .loaded and .ready that would be nice

Yeah, I really do tend to agree with that. Having to remember which one to use per API seems like a bad WTFWeb experience that will get us mocked in blog posts and conference talks for years to come.

@annevk

window/document.ready() seems bad given that onload means the same on both of them.

Right, that is the problem with that approach :(

Perhaps document.subresourcesReady or document.dependenciesReady or some such.

Do you think we should have both {window,document}.{ready,subresourcesReady}? Or just pick one base object? If so which one?

@jakearchibald

I still like @domenic's idea of .loaded being the less hand-wavey

I think this idea is only good if we can say there is always a .loaded so that web devs don't need to know about .ready in most cases, except maybe one special case of document/window which seems fine since it already has two 'loaded' events load and DOMContentLoaded. But, FontFaceSet makes such a world impossible. So converging on .ready seems more likely to work.

The promise property name should reflect the event name if one already exists

I disagree with this. It puts the legacy API in control of naming. Part of the issue we're trying to solve here is making things uniform, so that people don't have to remember DOMContentLoaded vs. load vs. loadingdone vs. success vs. ...

so .domReady or .ready is a better fit

That's an interesting idea. {window,document}.ready = load, {window,document}.domReady = DOMContentLoaded?

@annevk
Member
annevk commented Sep 15, 2015

I would just pick one object, probably document unless we also want this in workers somehow. If this needs to be something in workers, I'd go for self.

@zcorpan
Member
zcorpan commented Sep 15, 2015

I think we should not provide a promise for window.onload for now. For many use cases it is unnecessarily late, and it negatively impacts the user experience to wait longer than necessary, so maybe we shouldn't encourage it.

Workers don't have an equivalent of load/DOMContentLoaded, you can just do your stuff directly, and importScripts is sync...

@arturparkhisenko

Today in Polymer:

ready: function() {
    // `ready` is called after all elements have been configured, but
    // propagates bottom-up. This element's children are ready, but parents
    // are not.
},
attached: function() {
  // `attached` fires once the element and its parents have been inserted
  this.async(function() {
    // code that formerly resided in `domReady`
  });
}

document.ready promise will touch that?

@domenic
Member
domenic commented Sep 15, 2015

@ikeagold no, this has nothing to do with Polymer or web components.

@jonathantneal
Contributor

document.readyState already gives us two names, interactive and complete. Could these be reused? Example polyfill

@zcorpan
Member
zcorpan commented Sep 16, 2015

I think we shouldn't pay much attention to document.readyState since there are different readyStates on different things -- document, media elements, track, MediaController, script, EventSource, WebSocket, XHR... Only document uses interactive. It seems more clear to say "ready" than "interactive", IMO.

@hexalys
hexalys commented May 5, 2016 edited

@zcorpan

Only document uses interactive. It seems more clear to say "ready" than "interactive", IMO.

Indeed. I agree. But matching along consistent document.readyState is important for timing.

@annevk

The precedent of jQuery is ready = DOMContentLoaded.

FTR: jQuery.ready was initially meant to match the interactive state. It used DOMContentLoaded only due to persistent IE bugs with interactive up to IE10. Which lead to crappy results and poor performance for most jQuery plugins, when truly meant to bootstrap on interactive. I personally never, or very rarely wanted to use DOMContentLoaded to bootstrap plugins. I have always used interactive with hacks to fake-match interactive timing on IE6-10.

Most devs seem fairly ignorant as to the differences between interactive, DOMContentLoaded and complete states. If anything, I'd recommend for devs to stay away from DOMContentLoaded as first choice, with a preference for interactive. We need both though, to match timing context adequately.

So doc.ready semantic should stand for interactive (as per above ref of jQuery ongoing changes).

@WebReflection

DOMContentLoaded is already triggered when readyState is interactive. The main difference is that it's triggered after deferred scripts have been downloaded and parsed.

This is IMO the best place ever to be notified about document.ready because everyone can use deferred to inject non-blocking, crucial, dependencies upfront and use document.ready.then(bootstrap) to bring in any progressive enhancement to the page.

As summary, I think DOMContentLoaded is exactly where document.ready should be resolved.
If you truly need to configure something before, use deferred, which already does what you want, or use readystatechange and do whatever you need first time interactive is the current state.

Please let's keep this as simple as it has been kept for the last 10 years through DOMContentLoaded behavior which is just the right thing for this event.

@domenic
Member
domenic commented Oct 18, 2016 edited

We talked a bit about this at TPAC, and decided it was time to move forward. Here is my plan:

  • In general try to strongly parallel promise and event names. FontFaceSet and jQuery will not be used as precedents.
  • Tend toward more promises instead of a single "ready does the right thing" approach.

In particular:

  • Document:
    • document.interactive fulfills when readystate becomes "interactive"
    • document.contentLoaded fulfills when DOMContentLoaded does
    • document.loaded fulfills when readystate becomes "complete" = when load fires on window/document
    • None of these promises can ever reject
  • img, link, iframe, track, script, style:
    • Get a loaded promise, which fulfills when the load even would fire or rejects with some appropriate DOMException when the error event would fire
    • Some of these promises will "reset" to a new pending promise, in whatever process kicks off a new load (e.g. setting the src or href` attributes)
  • audio, video:
    • Do not get any promises, at least initially, as their state machine is too complicated (e.g. depends on current playback position) and they don't currently have a load event
  • embed, object, frame, input (type=image):
    • Do not get any promises, despite having load/error events, because they are too legacy

Any thoughts? I'd like to start working on a PR for this. /cc @dominiccooney

@hexalys
hexalys commented Oct 19, 2016

The Document event names makes sense. If opting to match document events. It might be appropriate to consider pageshow and pagehide as well, with a name match, or document.show and document.hide

@anthonyryan1

I agree with almost everything @domenic said.

I just want to throw in some concerns over the promises resetting when the href or src changes on img, script, etc. I feel that would potentially lead to confusing behavior and bugs. I've never seen a promise reset itself and resolve more than once and I don't think it's our place to be the first promise which will do that.

I think a better approach for addressing that case would be to require people to create a new element, swapping it for the element in the DOM tree to get a new promise & promise resolution they can wait for.

I'm also strongly apposed to a pageshow / pagehide promise for the same reason. A page can be shown or hidden multiple times. Promises are for things which can happen no more than once.

@gibson042
Contributor

Some of these promises will "reset" to a new pending promise, in whatever process kicks off a new load (e.g. setting the src or href` attributes)

Is that "reset" to be understood as applicable to the existing promises, or to the getter that supplies them (e.g., imgElement.loaded returning a new, distinct promise after src update)? And if the latter, would that reject previously-retrieved promises?

@annevk
Member
annevk commented Oct 19, 2016

@domenic per #127 (comment) <img> should maybe be more complicated. (We also need to decide HTMLDocument vs Document I suppose. Sooner the better if we start adding new properties.)

@domenic
Member
domenic commented Oct 19, 2016

I don't think we should make <img> more complicated for now. @zcorpan's sketch shows how we could expose the second load in the future, but that doesn't seem necessary.

All questions about the promise reset processing model are easier to answer with concrete spec text, so I won't really spend time going over that here.

@jonathantneal
Contributor

I’m so happy to see movement on document.ready. I want to chime in against img.ready for now. A document is one thing loaded once. Image can be many things loaded many times.

img.ready.then(onLoadCB, onErrorCB) is either confusing me now (which is fine), or it will confuse anyone who uses Promises (which is bad).

If we’re saying img.ready returns a new Promise whenever src changes, then it loses a ton of usefulness. For instance, if a site swaps out mangled base64 placeholders for real images then img.ready will not be helpful. If a site wants to track changes due to img.srcset then img.ready will not be helpful. Effectively, whenever a site swaps src for any reason, img.ready loses all helpfulness. Developers attracted to using the img.ready Promise anway would end up writing some sort of madness like img.addEventListener('onsrcchange', () => { img.ready = onLoadCB; } just to maintain their subscription.

Or, if we’re saying img.ready.then(onloadCB, onerrorCB) is able to fire onloadCB or onErrorCB multiple times, then we have changed how Promises are generally understood.


IMHO, the need for something like img.ready is still very real, but it should be addressed in a different issue. I hope it’s resolved with new, Promise-like listeners that replace the existing addEventListener/removeEventListener experience.

@domenic
Member
domenic commented Oct 19, 2016

You don't have to use image.loaded if it isn't useful for your use cases. For the vast majority of images whose src does not ever change more than once (from empty to a URL), it will serve well.

@domenic domenic self-assigned this Oct 19, 2016
@domenic
Member
domenic commented Oct 19, 2016

Some thoughts on the names for document's promises:

This is a classic web-specs "goodness vs. consistency" tradeoff. I am siding with consistency with the existing spec concepts and names (interactive, content loaded, and loaded).

The alternate world, where we go for good-but-inconsistent, is where we try to make up a nice set of names disconnected from the existing concepts. For example:

  • parsedAndSyncScriptsRun / parsedAndNonAsyncScriptsRun / loaded (very precise)
  • parsed / scriptsRun / loaded (compromise between preciseness and brevity)
  • parsed / ready / loaded (reuse jQuery's definition of "ready")
  • htmlLoaded / scriptsLoaded / subresourcesLoaded

But in the end I think consistency is more valuable here. Telling people "parsed will fulfill when the document's readystate becomes interactive" or "ready means DOMContentLoaded" or "subresourcesLoaded means the load event fired" is just kind of WTF. Sticking with "interactive will fulfill when the document's readystate becomes interactive", "contentLoaded will fulfill when DOMContentLoaded fires", and "loaded will fulfill when the load event fires" is a much simpler story.

This means we'll want to add a nice section to the spec explaining the difference between these three stages, since we won't be able to obviously infer them from their names. But I don't know if it was ever going to be that obvious, no matter what names we picked, and such sections are helpful anyway.

@domenic domenic added a commit that referenced this issue Oct 19, 2016
@domenic domenic Add document.{interactive,contentLoaded,loaded} promises
This closes #127, although that thread also contains discussion about
adding other "loaded" promises for various elements, which will move
elsewhere. For a discussion of the names, including why jQuery's "ready"
naming was not used, see
#127 (comment).
cfe9e1a
@domenic
Member
domenic commented Oct 19, 2016 edited

The pull request for the document promises is up at #1936. I'd like to add 1-3 small web-developer-facing examples of when you would use them, but am not able to come up with any great ones myself that are not instead better served by <script defer>. My best so far is something like:

<!-- in the head -->
<script>
const jsonPromise = fetch("some-data.json").then(r => r.json());
Promise.all([jsonPromise, document.interactive)]).then(([data]) => {
  // use data to manipulate the guaranteed-to-be-parsed contents of the document
});
</script>

and I guess you could say "in this example, substitute in contentLoaded if you only want to manipulate the document after crucial scripts have run, and use loaded for non-essential document manipulation that can happen after all subresources have loaded." What do people think of that? Do you have better scenarios?

@domenic
Member
domenic commented Oct 19, 2016

Ah, I see @jakearchibald has lots of nice examples at https://lists.w3.org/Archives/Public/public-whatwg-archive/2014Mar/0110.html, although they depend on the promises for links/images/etc. also being in place. Maybe it's best to hold off on examples until both are in the spec, and then we can use some of Jake's rather compelling and realistic examples.

@jakearchibald
Contributor

I have no memory of writing those.

@jakearchibald
Contributor

@domenic I like your example, but might be more readable as…

fetch("some-data.json").then(r => r.json()).then(data => {
  document.interactive.then(() => {
    document.querySelectorAll(".username").textContent = data.username;
  });
});

More nesting, but avoids the Promise.all and destructuring, which I had to squint at for a moment.

I dunno when it's acceptable to throw await in, but:

fetch("some-data.json").then(r => r.json()).then(async data => {
  await document.interactive;
  document.querySelectorAll(".username").textContent = data.username;
});

…reads well IMO.

@Krinkle
Member
Krinkle commented Oct 25, 2016

@jakearchibald I agree it's more readable indeed. Though Promise.all is imho a good general practice as it encourages declaring dependencies upfront and in one place.

It avoids the beginner's pitfall of making requests late instead of early (e.g. if two requests don't depend on each other, nesting would make the second request not start until after the first completes). If you swap fetch("some-data.json").then(r => r.json()) and document.interactive, the code still works, but finishes later due to the request starting much later.

Personally I've certainly too much code simply wrapping everything in a document-ready handler when it would make so much sense to fire off certain requests earlier. But I don't know if this anti-pattern justifies changing simple cases when they are "the right way around".

No strong preference one way or the other. Just thought I'd mention it. Perhaps removing the destructuring makes a good compromise?

For what it's worth, this is a common case where I enjoy the guilty pleasure of variadic arguments in jQuery.when() - elegantly adding void promises without visibly clobbering the callback signature.

$.when(
  $.ajax( .. ),
  $.ready
).then(data => {
  // ..
});
@jonathantneal
Contributor

Here’s a polyfill / prollyfill: https://github.com/jonathantneal/document-promises

I’ve subscribed to the PR in case there are any changes before the merge.

@domenic
Member
domenic commented Oct 25, 2016

Please, please, please please rename the polyfill, using e.g. a _ or $ prefix. If people start using that code before it is implemented in browsers, we will need to rename them to avoid conflicts with the existing usages.

@jdalton
jdalton commented Oct 25, 2016 edited

Please, please, please please rename the polyfill, using e.g. a _ or $ prefix.

Wouldn't checking that each property is nonexistent, instead of the single document.loaded check, before assigning them avoid issues, like with other shims?

@domenic
Member
domenic commented Oct 25, 2016

No; that's exactly the worst possible world, since then we can't change the semantics without also changing the name (since apps depend on the polyfill semantics, which aren't applied because something with different semantics but the same name appears).

Let's take this discussion to jonathantneal/document-promises#4. I am fully prepared to withdraw this proposal unless we can get these polyfills renamed or turned into a library that doesn't squat on global names, ASAP.

@jdalton jdalton referenced this issue in jonathantneal/document-promises Oct 25, 2016
Closed

Please rename ASAP #4

@jonathantneal
Contributor

Related to jonathantneal/document-promises#4, ponyfill changes were reviewed by @domenic, and then merged and published. You may proceed with the bettering of the internet.

@domenic domenic added a commit that referenced this issue Dec 9, 2016
@domenic domenic Add document.{interactive,contentLoaded,loaded} promises
This closes #127, although that thread also contains discussion about
adding other "loaded" promises for various elements, which will move
elsewhere. For a discussion of the names, including why jQuery's "ready"
naming was not used, see
#127 (comment).
dae43b1
@Krinkle
Member
Krinkle commented Jan 5, 2017 edited

How will these promises reflect on Navigation Timing? Right now it has two of these related events as part of its timing model to ensure the event processing is not wrongly attributed to the event before or after it.

  • responseEnd;
  • domLoading;
  • domInteractive;
  • domContentLoadedEventStart;
  • domContentLoadedEventEnd;
  • domComplete;
  • loadEventStart;
  • loadEventEnd;

This means that code running in a DOMContentLoaded event handler or window.onload event handler is measured and will execute before loadEventEnd. Presumably handlers for these two events would run at (almost?) the same time as when handlers for the document.contentLoaded and document.loaded promises would run.

I'm curious when the promise handlers would run. Also with regards to promises being async (though not sure if that matters given they're micro tasks?).

Actually, I'm also curious how the current readystatechange event handlers fit in with Navigation Timing. Are they part of reaching domInteractive and domComplete? Or between domInteractive/domContentLoadedEventEnd and domComplete/loadEventStart? Or is the 'complete' ready state change part of loadEvent? (since onload happens in the same task as as readystate changing to complete)

My main concern is ensuring that:

  • These promise handlers must not run after loadEventEnd so that loadEventEnd is still effective at measuring all time spent in code that doesn't explicitly start from a callback unrelated to page load (e.g. input event, storage event, fetch promise etc.).
  • Programatically stopping a browser at "page load" must not skip execution of these promise handlers (as long as the handlers were registered before resolving, of course).

/cc @jakearchibald @domenic

@domenic
Member
domenic commented Jan 5, 2017

@Krinkle I think the answers to your questions are pretty clearly spelled out in the proposed spec at #1936 as well as the existing spec for readystatechange.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment