The answer by @MikeGledhill (that got deleted) is essentially the beginning of the answer, though it could have explained it better, and browsers may not have all had the requestAnimationFrame
API available at that time:
Painting of pixels happens in the next animation frame. This means that if you call drawImage
, the screen pixels won't actually be updated at that time, but in the next animation frame.
There's no event for this.
But! We can use requestAnimationFrame
to schedule a callback for the next frame before paint (display update) happens:
myImg.onload = function() {
myContext.drawImage(containerImg, 0, 0, 300, 300);
requestAnimationFrame(() => {
// This function will run in the next animation frame, *right before*
// the browser will update the pixels on the display (paint).
// To ensure that we run logic *after* the display has been
// updated, an option is to queue yet one more callback
// using setTimeout.
setTimeout(() => {
// At this point, the page rendering has been updated with the
// `drawImage` result (or a later frame's result, see below).
}, 0)
})
};
What is happening here:
The requestAnimtionFrame
call schedules a function that will be called right before the browser updated display pixels. After this callback is completed, the browser will continue to synchronously update the display pixels in a following tick that is very similar to a microtask.
The "microtask"-like in which the browser updates the display, happens after your requestAnimationFrame
callback, and happens after all user-created microtasks that a user creates in the callback using Promise.resolve().then()
or an await
statement. This means one cannot make deferred code fire immediately (synchronously) after the paint task happens.
The only way to guarantee that logic will fire after the next paint task, is to use setTimeout
(or a postMessage
trick) to queue a macrotask (not microtask) from an animation frame callback. A macrotask queued from a requestAnimationFrame
callback will fire after all microtasks and microtask-likes, including the task that updates the pixels. The setTimeout (or postMessage) macrotask will not fire synchronously after animation frame microtasks.
This approach is not perfect though. Most of the time, the macrotask queued from setTimeout
(and more likely with postMessage
) will fire before the next animation frame and paint cycle. But, due to the specification of setTimeout
(and postMessage
), there is no guarantee that the delay will be exactly what we specify (0
in this example), and the browser is free to use heuristics and/or hard-coded values like 2ms to determine when is the soonest time to run a setTimeout
(macrotask) callback.
Due to this non-guaranteed non-synchronous nature of macrotask scheduling, it is possible, though in practice unlikely, that your setTimeout
(or postMessage
) callback can fire not just after the current animation frame (and the paint cycle that updates the display), but after the next animation frame (and its paint task), meaning that a macrotask callback has a small chance firing too late for the frame you were targeting. This chance is reduced when using postMessage
instead of setTimeout
.
That being said, this sort of thing is probably something you should not do unless you're trying to write tests that capture painted pixels and compare them to expected results or something similar.
In general, you should schedule any drawing logic (f.e. ctx.drawImage()
) using requestAnimationFrame
, never rely on the actual timing of the paint update, and assume that the user will see what the browser APIs guarantee you've specified for them to see (the browsers have their own tests in place for ensuring their APIs work).
Finally, we don't know what your actual goal is. Most likely this answer may be irrelevant to that goal.
Here's the same example using the postMessage
trick:
let messageKey = 0
myImg.onload = function() {
myContext.drawImage(containerImg, 0, 0, 300, 300);
requestAnimationFrame(() => {
// This function will run in the next animation frame, *right before*
// the browser will update the pixels on the display (paint).
const key = "Unique message key for after paint callback: "+ messageKey++
// To ensure that we run logic *after* the display has been
// updated, an option is to queue yet one more callback
// using postMessage.
const afterPaint = (event) => {
// Ignore interference from any other messaging in the app.
if (event.data != key) return
removeEventListener('message', afterPaint)
// At this point, the page rendering has been updated with the
// `drawImage` result (or a later frame's result, but
// more unlikely than with setTimeout, as per above).
}
addEventListener('message', afterPaint)
// Hack: send a message which arrives back to us in a
// following macrotask, more likely sooner than with
// setTimeout.
postMessage(key, '*')
})
};