Closed Bug 1571756 Opened 8 months ago Closed 6 months ago

Menu on website gets into a resize loop

Categories

(Web Compatibility :: Desktop, defect, P1)

Unspecified
All
defect

Tracking

(firefox-esr60 unaffected, firefox-esr68 fixed, firefox68 wontfix, firefox69 fixed, firefox70 fixed, firefox71 fixed)

RESOLVED FIXED
Tracking Status
firefox-esr60 --- unaffected
firefox-esr68 --- fixed
firefox68 --- wontfix
firefox69 --- fixed
firefox70 --- fixed
firefox71 --- fixed

People

(Reporter: gcp, Unassigned)

References

(Regression, )

Details

(Keywords: regression, webcompat:needs-diagnosis)

Attachments

(1 file)

macOS 14.10, current Nightly

Load: https://www.bbc.com/worklife/article/20190801-the-trick-that-makes-you-overspend

Resize browser a bit so the first row (the menu) just doesn't fit. The site starts shimmering as it enters an eternal resizing loop.

dom.block_reload_from_resize_event_handler has no effect.

Didn't observe this behavior in Chome.

If that pref has no effect, then it's not it. Also it seems there are no reloads involved, just either a layout bug or the site JS doing something weird.

That being said, it'd be nice to know if this is a regression.

No longer blocks: 1570566

Ah, I'm sorry :emilio! Just saw it in Slack and I wasn't aware that the Blocks status depends whether it is still affected by the pref or not.

fwiw this is happening in Firefox 68.0.1 (64-bit) on macOS too.

A performance profile
https://perfht.ml/2GQQwDz

I had a narrow window, then expanded, then narrowed again. The janking effect on the menu is happening only when the switch to the new layout is happening. then stops right away when we come back to the initial layout.

Maybe related
https://www.bbc.com/worklife/assets/static/js/bundle.581dde53.js
as it seems to happen when there is a change of mediaqueries

                                                  f = function () {
                                                    function e(e, t, n) {
                                                      var r = this;
                                                      this.nativeMediaQueryList = e.matchMedia(t),
                                                      this.active = !0,
                                                      this.cancellableListener = function () {
                                                        r.matches = r.nativeMediaQueryList.matches,
                                                        r.active && n.apply(void 0, arguments)
                                                      },
                                                      this.nativeMediaQueryList.addListener(this.cancellableListener),
                                                      this.matches = this.nativeMediaQueryList.matches
                                                    }
                                                    return e.prototype.cancel = function () {
                                                      this.active = !1,
                                                      this.nativeMediaQueryList.removeListener(this.cancellableListener)
                                                    },
                                                    e
                                                  }(),

When this is called.

                                                      this.cancellableListener = function () {
                                                        r.matches = r.nativeMediaQueryList.matches,
                                                        r.active && n.apply(void 0, arguments)
                                                      },

r.nativeMediaqueryList.matches is called and this seems to happen only in between (r.nativeMediaqueryList.media )

(min-width: 600px) and (max-width: 1279px)
and
(min-width: 600px) and (max-width: 1007px)

Let's log r.nativeMediaqueryList.media from the max window to min window.

08:38:22.368 (min-width: 600px) and (max-width: 1279px) bundle.581dde53.js:formatted:33705
08:38:22.373 (min-width: 1008px) and (max-width: 1279px) bundle.581dde53.js:formatted:33705

// happening in this range only.

08:38:55.630 (min-width: 600px) and (max-width: 1007px) bundle.581dde53.js:formatted:33705
08:38:55.756 (min-width: 1008px) 2 bundle.581dde53.js:formatted:33705
08:38:55.759 (max-width: 1007px) bundle.581dde53.js:formatted:33705
08:38:55.762 (min-width: 1008px) and (max-width: 1279px) bundle.581dde53.js:formatted:33705
08:38:55.764 (min-width: 768px) and (max-width: 1007px) bundle.581dde53.js:formatted:33705
08:39:29.249 (min-width: 768px) 2 bundle.581dde53.js:formatted:33705
08:39:29.252 (max-width: 767px) bundle.581dde53.js:formatted:33705
08:39:29.253 (min-width: 600px) and (max-width: 767px) bundle.581dde53.js:formatted:33705
08:39:29.254 (max-width: 767px) 2 bundle.581dde53.js:formatted:33705
08:39:29.256 (min-width: 768px) bundle.581dde53.js:formatted:33705
08:39:29.258 (min-width: 768px) and (max-width: 1007px) bundle.581dde53.js:formatted:33705
08:39:29.259 (max-width: 767px) bundle.581dde53.js:formatted:33705
08:39:33.754 (max-width: 599px) bundle.581dde53.js:formatted:33705
08:39:33.755 (min-width: 600px) and (max-width: 1007px) bundle.581dde53.js:formatted:33705
08:39:33.756 (min-width: 600px) and (max-width: 767px) bundle.581dde53.js:formatted:33705
08:39:33.757 (min-width: 600px) and (max-width: 1279px) bundle.581dde53.js:formatted:33705
08:39:37.980 (min-width: 300px) and (max-width: 399px) 2 bundle.581dde53.js:formatted:33705

btw this stops as soon as i set


/* Inline #5 | https://www.bbc.com/worklife/article/20190801-the-trick-that-makes-you-overspend */

.nav-build-bar__links {
  min-width: 0;
}

That might be interesting for Daniel.

Flags: needinfo?(dholbert)

I can reproduce the issue on Windows10 Nightly70.0a1.

Regression window:
https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml?fromchange=805f1a2737a0ef58a64cf8e7b6239543d4b45940&tochange=dc6c04a63309d3461c0ca9dc23ae186a91deb8c6

Triggered by:
dc6c04a63309d3461c0ca9dc23ae186a91deb8c6 Robert Longson — Bug 1396642 - support smaller viewBox coordinates at the expense of larger ones r=dholbert

OS: macOS → All
Regressed by: 1396642

Is this a valid regression or is it a site issue?

Sorry, I missed the needinfo. Is this still reproducible? I'm having trouble reproducing it. If so and if Bug 1396642 is really the commit blamed via mozregression, I'm guessing it was just unlucky, and the site could already have gotten into this state if it used slightly different markup.

If it is reproducible, could someone attach a screenshot and/or screencast of the Firefox window when the bad thing is happening? That would help me know how wide to make the window when trying to test. Comment 0 says "Resize browser a bit so the first row (the menu) just doesn't fit", but this is hard to do because the site is very responsive and goes through a series of different versions of that menu with progressively less + smaller content so that it always fits. :) I'm not sure which transition-to-a-smaller-layout is the sometimes-problematic one.

Flags: needinfo?(dholbert) → needinfo?(gpascutto)
Attached video screencast

I can reproduce the issue on Nightly71.0a1.

Build ID 20190917093629
User Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0

FYI, In my case,
If the following svg viewBox properties was changed to "0 0 107.9 16" from "0 0 107.9 15.8", then the issue was gone.

<svg version="1.1" id="Layer_1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 107.9 15.8" style="enable-background:new 0 0 107.9 15.8;" xml:space="preserve">

...

Component: Desktop → SVG
Product: Web Compatibility → Core
Flags: needinfo?(gpascutto) → needinfo?(dholbert)

Odd... I still can't reproduce on Linux Nightly 2019-09-17. Tested on my desktop (1:1 DPI) and my laptop (HiDPI, tested with 200% and 300% scaling).

In comment 10, the threshold seems to be the point where "Bright Sparks" appears/disappears (to the right of "Worklife 101"). I was not able to reproduce any flickering when resizing my window around that threshold.

A few thoughts:

  • This might be font-dependent (which could explain why I haven't been able to repro on Linux)
  • There seems to be a bit of "fudge factor" in their decision of when to show/hide the Bright Sparks text. In Responsive Design Mode, starting with a wide viewport, the "Bright Sparks" is visible with viewport-width=896px and disappears when I drop to 895px -- but then if I go back up to 896px, it doesn't reappear until I get to 897px.

Anyway -- comment 11 is very helpful in explaining how Bug 1396642 may have impacted things here. Before Bug 1396642, we probably rounded the given viewBox values (0 107.9 15.8) to 0 0 108 16 under the hood. Bug 1396642's change in behavior (supporting finer-grained values) will have made the sizing calculations more precise & slightly different here. So whatever measurement BBC's JS is doing to make the call about whether to show or hide this content, it will be getting slightly different measurements now.

And to be clear, the content is hiding/showing due to JS that the site is running -- they actually move the element back and forth between different places in the DOM entirely. They hide it by moving it to a different container with class=nav-build-bar__links-hidden which is display:none -- that's how it disappears. So I think this is just a WebCompat issue, and likely a bug in the site's JS.

Flags: needinfo?(dholbert)

(In reply to Karl Dubost💡 :karlcow from comment #5)

btw this stops as soon as i set


/* Inline #5 | https://www.bbc.com/worklife/article/20190801-the-trick-that-makes-you-overspend */

.nav-build-bar__links {
  min-width: 0;
}

That might be interesting for Daniel.

This doesn't actually fix the issue, I don't think. It just prevents them from ever hiding "Bright Sparks" at all (for me at least) -- it ends up overflowing and overlapping the hamburger menu etc. as you shrink the page size. It eventually disappears after a media query is triggered (I think), but not until much later than their dynamic-hiding magic would like for it to be hidden.

Component: SVG → Desktop
Product: Core → Web Compatibility
Hardware: x86_64 → Unspecified

Shot in the dark: they might have a rounding error in whatever JS math that they use when they're estimating how many elements will fit into the available space. They probably detect overflow, and then remove the "Bright Sparks" element (for example) and then re-run their JS to reevaluate-how many elements will fit, and then they mistakenly think the removed element would fit without overflowing, and they add back the removed element, and repeat.

Ksenia, can you try to dig in here?

Flags: needinfo?(kberezina)
Priority: -- → P1

Odd... I still can't reproduce on Linux Nightly 2019-09-17.

I can't either (on Linux), but it still reproduces on an older Macbook Pro (which has twice the resolution!). Is there something I can check for you?

This is the function that is causing the loop:

{
  key: "updateDimensions",
  value:
function () {
            var e = this,
                t = this.navBar.current;
            if (t) {
                var n = this.navBarLinks.current,
                    r = this.navBarOpenSponsored.current,
                    i = this.navBarHideLinks.current,
                    a = this.navBarBranding.current,
                    l = t.offsetWidth,
                    s = this.state,
                    u = s.firstSetup,
                    c = s.sizeOfOldElements,
                    f = 0;
                if (u) {
                    for (this.setState({
                        firstSetup: !1,
                        sizeOfOldElements: []
                    }); i.firstChild;) i.removeChild(i.firstChild);
                    f = 15000
                }
                var d = n.offsetWidth + r.offsetWidth + a.offsetWidth + f;
                if (d > l || this.props.mobile) for (; d > l && n.lastChild && null !== n.lastChild && void 0 !== n.lastChild || this.props.mobile && n.lastChild && null !== n.lastChild && void 0 !== n.lastChild;) !function () {
                    var t = n.lastChild;
                    e.setState(function (e) {
                        return {
                            sizeOfOldElements: [].concat(o(e.sizeOfOldElements), [
                                t.offsetWidth
                            ])
                        }
                    }),
                        d -= t.offsetWidth,
                        i.insertBefore(t, i.firstChild)
                }();
                else null !== i.firstChild && d + c[c.length - 1] <= l && (this.setState(function (e) {
                    var t = e.sizeOfOldElements.slice();
                    return t.pop(),
                        {
                            sizeOfOldElements: t
                        }
                }), null !== i.firstChild && i.firstChild && void 0 !== i.firstChild && n.appendChild(i.firstChild))
            }
        }
}

where "Bright Sparks" link offsetWidth is stored in a component state and then the element is being inserted to a hidden div ( so "Bright Sparks" disappears):

var t = n.lastChild;
        e.setState(function (e) {
                return {
                     sizeOfOldElements: [].concat(o(e.sizeOfOldElements), [
                           t.offsetWidth
                     ])
                }
         }),
        d -= t.offsetWidth,
       i.insertBefore(t, i.firstChild)

since there is a change to a state, componentDidUpdate is triggered:

key: "componentDidUpdate",
           value: function() {
                  setTimeout(this.handleResize, 0)
            }
},
{
key: "handleResize",
         value: function() {
               this.updateDimensions()
         }
 }

And updateDimensions function runs again, but this time this condition is being executed:

else null !== i.firstChild && d + c[c.length - 1] <= l && (this.setState(function (e) {
                    var t = e.sizeOfOldElements.slice();
                    return t.pop(),
                        {
                            sizeOfOldElements: t
                        }
                }), null !== i.firstChild && i.firstChild && void 0 !== i.firstChild && n.appendChild(i.firstChild))

so here "Bright Sparks" is being inserted back to navigation links and this.setState is being run again, triggering the loop over and over.

This is not happening in Chrome, though, so I've compared the values. d + c[c.length - 1] is 945 in Chrome and 944 in Firefox, while l (the wrapping nav bar innerWidth) is 944.
While d (the sum of innerWidths of logo, links and menu elements) is the same in both browsers, c (the innerWidth of hidden "Bright Sparks" ) is different. It's 124.5 in Chrome, they round it to 125 and it's 124.467 in Firefox, we round it to 124. Perhaps, the site shouldn't rely on innerWidth here. My guess is to use getBoundingClientRect().width instead.

As for the regression, the menu innerWidth (svg) has changed as Daniel mentioned in comment 12 - it was smaller before the regression, so I assume (the debbugger doesn't want to work in that version of FF) that in this case the condition if (d > l || this.props.mobile) wasn't satisfied and the first setState never got to run. As a result, no infinite loops :)

UPDATE: contacted the site via mailing list

Flags: needinfo?(kberezina)

Looks like this was fixed

Status: NEW → RESOLVED
Closed: 6 months ago
Resolution: --- → FIXED
You need to log in before you can comment on or make changes to this bug.