webcompat / web-bugs

A place to report bugs on websites.
https://webcompat.com
Mozilla Public License 2.0
728 stars 63 forks source link

any-website - ResizeObserver behaves differently across browsers #117611

Closed trusktr closed 1 year ago

trusktr commented 1 year ago

URL: https://any-website

Browser / Version: Chrome 109.0.0 Operating System: Mac OS X 10.15.7 Tested Another Browser: Yes Safari and Firefox

Problem type: Something else Description: ResizeObserver behaves differently across browsers Steps to Reproduce: ResizeObserver does not behave the same across browsers. Try the following code in Chrome console, Safari console, and Firefox console and not differences across browsers:

const o = new ResizeObserver((changes) => {
    console.log('resize', changes)
    o.disconnect()
})

console.log('made new observer')

const el = document.createElement('div')
o.observe(el)

console.log('observe el')

queueMicrotask(() => console.log('microtask'))

setTimeout(() => console.log('timeout'))

requestAnimationFrame(() => console.log('animation frame'))

setTimeout(() => {
  console.log('append')
  el.style.width = '100px'
  el.style.height = '100px'
  document.body.append(el)
}, 2000)

Output in Safari:

made new observer
observe el
microtask
timeout
animation frame
append
resize [ ResizeObserverEntry ]

Output in Chrome and Firefox:

made new observer
observe el
microtask
timeout
animation frame
resize [ ResizeObserverEntry ]
append

The last two things in the output are swapped between Safari and Chrome/Firefox.

Safari has the best behavior, because it is the only browser that fires the resize callback when the element has changed to a size of 100x100.

But maybe all browsers are incorrect: maybe they should all fire initially like Chrome and Firefox, and then they should all fire again on element connected.

Browser Configuration
  • None

From webcompat.com with ❤️

softvision-raul-bucata commented 1 year ago

@denschub Can you take a look over this , please?

denschub commented 1 year ago

Ah interesting edge-case! The very quick tl;dr is: Safari is about to change its behavior here.

The longer version is: An empty element not added to the DOM has a 0x0 width. ResizeObservers work by comparing an element's current size to a lastReportedSize, and if the current element size is different from the lastReportedSize, it will fire the observer.

The current spec draft says to initialize this lastReportedSize value with (0, 0). So that would match what Safari is doing - the element's size hasn't changed away from (0, 0), so no notification should be dispatched.

The catch here, however, is that the CSSWG decided to change this initial value to (-1, -1) in August last year. With that in mind, Firefox and Chrome are correct. Also, there is a two weeks old patch for WebKit in-flight that will bring Safari in alignment with that. So that means it's expected for the ResizeObserver to fire, and reading this spec discussion, it looks like this is intended. However-however, the same issue also continues this discussion and there is no clear outcome yet. So ultimately, this might change again.

For now, however, Firefox and Chrome are "correct", and Safari is on its way to having the same behavior.

@trusktr, if you have an actual site/app that breaks because of this, please share details, so we can raise the priority of the impl-bugs accordingly. For now, though, there is nothing actionable here since this is already worked out and patches are in-flight, so I'll close this issue. (we can always re-open if there is evidence for a real-world site/app breaking)

trusktr commented 1 year ago

@denschub Thanks for explaining all that! The consistency with all browsers will be good, but the current Firefox and Chrome behavior is broken then: when the element is appended to DOM, and the size has changed to 100x100, the callback does not fire. In that sense, Safari's behavior is waaay better, because that's the size we need to handle.

I don't have a site published publicly experiencing issues (or maybe I do, I will investigate existing bugs now), but I've got some things in the works for LUME (I need to ensure that I render WebGL after resize, not during animation frames, or else it means for every resize there will be two canvas draws instead of one (ouch, almost ready to publish this article on how ResizeObserver "causes" this issue with canvas)), and an internal project at SpaceX, and I've made a tool called requestResizeObserverFrame which, as you might guess, is similar to requestAnimationFrame but you get a callback that runs after size observations. So in making this function, I've noticed the browser differences.

denschub commented 1 year ago

@trusktr yeah, I agree that this feels unintuitive. I, too, would expect the observer to fire after the element has been added to the DOM, since it does change its size.

Given the large number of open spec issues for ResizeObservers, I'm not 100% sure which bug would be the right to follow or participate. @emilio, I've seen you participate there, can you shed some light into this behavior?

emilio commented 1 year ago

That behavior is intended, and consistent with IntersectionObserver. Otherwise there's no way to observe an element getting inserted etc.

The callback in the example doesn't fire because the observer is disconnected after the first observation? If you remove the o.disconnect() line resizes are reported as expected.

denschub commented 1 year ago

The callback in the example doesn't fire because the observer is disconnected after the first observation? If you remove the o.disconnect() line resizes are reported as expected.

🤦 Yes. You're right, of course. My brain didn't compute, this makes sense, then.

trusktr commented 1 year ago

Doh, I totally overlooked that, thanks @emilio! In that case yeah, it looks like Safari is just missing one resize observation initially, and otherwise all browsers fire when changed to 100x100. Updated code:

const o = new ResizeObserver((changes) => {
    console.log('resize', changes)
    // o.disconnect()
})

console.log('made new observer')

const el = document.createElement('div')
o.observe(el)

console.log('observe el')

queueMicrotask(() => console.log('microtask'))

setTimeout(() => console.log('timeout'))

requestAnimationFrame(() => console.log('animation frame'))

setTimeout(() => {
  console.log('append')
  el.style.width = '100px'
  el.style.height = '100px'
  document.body.append(el)
  // ... all browsers log the final 100x100 resize
}, 2000)