Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add document.ready promise #127

Open
tabatkins opened this issue Sep 10, 2015 · 57 comments · May be fixed by #1936
Open

Add document.ready promise #127

tabatkins opened this issue Sep 10, 2015 · 57 comments · May be fixed by #1936
Assignees
Labels
addition/proposal New features or enhancements

Comments

@tabatkins
Copy link
Contributor

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
Copy link
Member

annevk commented Sep 10, 2015

So what would the equivalent be for DOMContentLoaded?

@foolip
Copy link
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
Copy link
Member

annevk commented Sep 10, 2015

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

@jakearchibald
Copy link
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
Copy link
Contributor

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

@foolip
Copy link
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
Copy link
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
Copy link
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
Copy link

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
Copy link
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
Copy link
Contributor

That fits with onload. Makes sense.

@domenic domenic added the addition/proposal New features or enhancements label Sep 10, 2015
@foolip
Copy link
Member

foolip commented Sep 11, 2015

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

@jakearchibald
Copy link
Contributor

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

@zcorpan
Copy link
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
Copy link
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
Copy link
Member

zcorpan commented Sep 14, 2015

No particular need for a distinct object.

@annevk
Copy link
Member

annevk commented Sep 14, 2015

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

@zcorpan
Copy link
Member

zcorpan commented Sep 14, 2015

@annevk Yes.

@domenic
Copy link
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
Copy link
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
Copy link
Contributor Author

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
Copy link

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

@foolip
Copy link
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
Copy link
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
Copy link
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
Copy link
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
Copy link
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
Copy link
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.

@domenic
Copy link
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
Copy link
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
Copy link
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
Copy link
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 added a commit that referenced this issue Oct 19, 2016
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).
@domenic domenic linked a pull request Oct 19, 2016 that will close this issue
4 tasks
@domenic
Copy link
Member

domenic commented Oct 19, 2016

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
Copy link
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
Copy link
Contributor

I have no memory of writing those.

@jakearchibald
Copy link
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
Copy link
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 seen 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 => {
  // ..
});

@jakearchibald
Copy link
Contributor

@Krinkle fwiw, I tackled this point over at https://developers.google.com/web/fundamentals/getting-started/primers/async-functions#careful_avoid_going_too_sequential

@jonathantneal
Copy link
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
Copy link
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
Copy link

jdalton commented Oct 25, 2016

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
Copy link
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.

@jonathantneal
Copy link
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 added a commit that referenced this issue Dec 9, 2016
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).
@Krinkle
Copy link
Member

Krinkle commented Jan 5, 2017

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
Copy link
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
Labels
addition/proposal New features or enhancements
Development

Successfully merging a pull request may close this issue.