Closed Bug 1629101 Opened 4 years ago Closed 4 years ago

Slow scrolling on Reddit chat

Categories

(Web Compatibility :: Site Reports, defect)

defect
Not set
normal

Tracking

(Performance Impact:?)

RESOLVED DUPLICATE of bug 1392460
Performance Impact ?

People

(Reporter: miketaylr, Unassigned)

References

()

Details

Originally reported @ https://github.com/webcompat/web-bugs/issues/51383

STR:

  1. go to https://new.reddit.com/r/firefox/comments/fy1bqf/firefox_75_release_chat/
  2. scroll up

Note: this repros for me on macOS with a mousewheel, but not the touchpad. The original reporter reported on Linux, with a touchpad.

They recorded some videos too:

Firefox https://i.imgur.com/ynJNd6w.mp4
Chrome https://i.imgur.com/QbOlJul.mp4

I recorded a profile from my machine (unsure if it's useful):
https://perfht.ml/3c8XIrK

To note, the recordings are of Firefox 75.0 and Chrome 80.X, respectively. They were recorded on a Windows system, and a touchpad is being used to scroll in them.

I should clarify that I'm not the user who submitted the report; I did originally mention it and record a couple of examples (the imgur links here). The user, who was able to reproduce the issue on their Linux system, subsequently filed the report.

User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0
Integrated Graphics, Intel HD 620

This appears to be a bug on the reddit side. They have a wheel event listener that is canceling the wheel event and driving scrolling directly. That much I could confirm via apz.inputqueue logging. The JS code is obfuscated with webpack and neither Chrome nor Firefox devtools can show me the relevant functions, but I suspect they are assuming the scroll deltas in the wheel events are in lines when they might be in pixels. We've seen similar bugs in the past.

Component: Panning and Zooming → Desktop
Product: Core → Web Compatibility

The event is added in
https://www.redditstatic.com/desktop2x/ChatPost.d981458441a775d647b7.js

    class enextends s.a.Component{
      constructor(e) {
        var t;
        super (e),
        t = this,
        this._ref = s.a.createRef(),
        this._refBeforeActiveComments = s.a.createRef(),
        this._refNextActiveComments = s.a.createRef(),
        this.chunkSize = 50,
        this.loadedMore = !1,
        this.scrollTop = () =>this.$ref ? Math.ceil(this.$ref.scrollTop)  : 0,
        this.scrollHeight = () =>this.$ref ? this.$ref.scrollHeight : 0,
        this.clientHeight = () =>this.$ref ? this.$ref.clientHeight : 0,
        this.scrolledPosition = () =>this.scrollHeight() - this.clientHeight() - this.scrollTop(),
        this.scrolledToTop = () =>0 === this.scrollTop(),
        this.scrolledToBottom = function () {
          let e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 0;
          return e >= t.scrolledPosition()
        },
        this.scrollToLastBottomChunk = () =>{
          const e = this.state.chunks.length - 1;
          e !== this.state.activeChunkIndex ? this.setState({
            activeChunkIndex: e
          }, () =>{
            this.scrollToBottom(),
            this.hideNewComments(!0)
          })  : (this.scrollToBottom(), this.hideNewComments(!0))
        },
        this.addScrollListener = () =>this.$ref && this.$ref.addEventListener('wheel', this.onScroll),
        this.removeScrollListener = () =>this.$ref && this.$ref.removeEventListener('wheel', this.onScroll),
        this.preventParentScroll = e=>{
          e.preventDefault();
          const t = this.$ref && this.$ref.scrollTop + e.deltaY;
          this.scrollTo(t || 0)
        },
        this.shouldLoadMoreData = () =>{
          this.props.loadMore && this.scrolledToTop() && this.props.loadMore(),
          this.loadedMore = !(!this.scrolledToTop() || !this.props.hasMoreComments)
        },
        this.onScroll = e=>{
          this.preventParentScroll(e),
          this.setNextTopChunk(),
          this.setNextBottomChunk(),
          this.shouldLoadMoreData(),
          this.hideNewComments()
        },
        this.state = {
          initialized: !1,
          list: this.props.children,
          chunks: this.splitChunks([...this.props.children]),
          activeChunkIndex: 0,
          newCommentsCount: 0
        }
      }
      get $ref() {
        return this._ref.current
      }
      get $refBeforeActiveComments() {
        return this._refBeforeActiveComments.current
      }
      get $refNextActiveComments() {
        return this._refNextActiveComments.current
      }
      scrollTo(e) {
        this.$ref && (this.$ref.scrollTo ? this.$ref.scrollTo({
          top: e
        })  : this.$ref.scrollTop = e)
      }
      scrollToBottom() {
        this.$ref && (this.$ref.scrollTo ? this.$ref.scrollTo({
          top: this.scrollHeight() - this.clientHeight()
        })  : this.$ref.scrollTop = this.scrollHeight())
      }
      scrolledToFirstTopChunk() {
        return !this.props.hasMoreComments && this.state.activeChunkIndex <= 1
      }
      scrolledToLastBottomChunk() {
        const e = this.state.chunks.length - 1;
        return this.state.activeChunkIndex + 1 >= e
      }
      scrollToTargetComment(e) {
        const t = Math.floor(this.clientHeight() / 2);
        if (void 0 !== e) return void this.scrollTo(e - t);
        const {
          targetCommentIndex: n,
          children: o
        }
        = this.props;
        if (o && o.length && void 0 !== n && o[n]) {
          const e = Math.floor(n / this.chunkSize);
          this.state.activeChunkIndex !== e && this.setState({
            activeChunkIndex: e
          });
          const o = document.querySelector('#targetComment').offsetTop;
          o > t ? this.scrollTo(o - t)  : this.scrollTo(o)
        }
      }
      hideNewComments(e) {
        (this.state.newCommentsCount > 0 && this.scrolledToBottom() || e) && this.setState({
          newCommentsCount: 0
        })
      }
      splitChunks(e) {
        const t = [
        ];
        for (; e.length; ) t.push(e.splice(0, this.chunkSize));
        return t
      }
      setNextBottomScroll() {
        const e = this.$refNextActiveComments ? this.$refNextActiveComments.clientHeight : 0;
        this.scrollTo(this.scrollHeight() - this.clientHeight() - e)
      }
      setNextBottomChunk() {
        if (this.scrolledToBottom()) {
          const e = this.state.activeChunkIndex + 1,
          t = this.state.chunks.length - 1,
          n = e < t ? e : t;
          this.setState({
            activeChunkIndex: n
          }, this.setNextBottomScroll)
        }
      }
      setNextTopScroll() {
        this.scrollTo(this.$refBeforeActiveComments && this.$refBeforeActiveComments.clientHeight || 0)
      }
      setNextTopChunk() {
        if (this.scrolledToTop()) {
          const e = this.state.activeChunkIndex - 1,
          t = this.state.chunks.length - 1,
          n = t > e ? e : t;
          e >= 0 && this.setState({
            activeChunkIndex: n
          }, this.setNextTopScroll)
        }
      }
      getChunkIndexOnUpdate(e, t) {
        if (this.props && this.props.children.length && this.props.children[0] && this.props.children.length - e.children.length > 1) {
          const e = t.length - this.state.chunks.length,
          n = this.state.activeChunkIndex + e;
          if (e > 1) return n
        }
      }
      componentDidMount() {
        this.addScrollListener(),
        this.scrollToLastBottomChunk(),
        this.setState({
          initialized: !0
        }),
        this.scrollToTargetComment()
      }
      componentWillUnmount() {
        this.removeScrollListener()
      }
      getSnapshotBeforeUpdate(e) {
        const t = e.children.length !== this.props.children.length || !Kt() (C() (e.children), C() (this.props.children)) || !Kt() (zt() (e.children), zt() (this.props.children));
        return t ? {
          childrenAreNotEqual: t,
          scrolledBottom: this.scrolledToBottom(30)
        }
         : null
      }
      componentDidUpdate(e, t, n) {
        if (n && n.childrenAreNotEqual) {
          const t = this.splitChunks([...this.props.children]),
          o = this.getChunkIndexOnUpdate(e, t),
          s = this.props.children.length - e.children.length;
          this.setState({
            list: this.props.children,
            chunks: t,
            activeChunkIndex: o || this.state.activeChunkIndex,
            newCommentsCount: this.scrolledToBottom() || 1 !== s || this.loadedMore ? this.state.newCommentsCount : this.state.newCommentsCount + s
          }, () =>{
            o && this.setNextTopScroll(),
            n.scrolledBottom && this.scrollToLastBottomChunk(),
            this.loadedMore = !1
          })
        }
        this.scrollToTargetComment()
      }
      render() {
        const {
          className: e,
          isLivestreaming: t
        }
        = this.props,
        n = this.state.newCommentsCount > 0 ? Object(X.a) ([Zt.a.NewComments,
        Zt.a.show])  : Zt.a.NewComments;
        return s.a.createElement(s.a.Fragment, null, s.a.createElement($t, {
          className: e,
          key: 'chatScroller',
          chunkSize: this.chunkSize,
          isLoading: !this.state.initialized || !!this.props.isLoading,
          isPrevLoading: !this.scrolledToFirstTopChunk(),
          isLivestreaming: t,
          isNextLoading: !this.scrolledToLastBottomChunk(),
          setRef: this._ref
        }, s.a.createElement('div', {
          key: 'beforeActiveCommentsSection',
          ref: this._refBeforeActiveComments,
          className: Zt.a.ScrollerSection
        }, this.state.chunks[this.state.activeChunkIndex - 1]), s.a.createElement('div', {
          key: 'activeCommentsSection',
          className: Zt.a.ScrollerSection
        }, this.state.chunks[this.state.activeChunkIndex]), s.a.createElement('div', {
          key: 'nextActiveCommentsSection',
          ref: this._refNextActiveComments,
          className: Zt.a.ScrollerSection
        }, this.state.chunks[this.state.activeChunkIndex + 1])), s.a.createElement(Qt, null, s.a.createElement(Gt.f, {
          className: n,
          onClick: () =>this.scrollToLastBottomChunk()
        }, this.state.newCommentsCount, ' ', Z.fbt._({
          '*': 'NEW MESSAGES',
          _1: 'NEW MESSAGE'
        }, [
          Z.fbt._plural(this.state.newCommentsCount)
        ], {
          hk: '1bTJTr'
        }), '↓')))
      }
    }

Thanks for tracking that down! As you can see, the preventParentScroll code uses e.deltaY assuming it's in pixels when in fact it may be in lines.

so similar to Bug 1541361, Bug 1448830, Bug 1350616,
and probably related to Bug 1392460

https://www.redditstatic.com/desktop2x/ChatPost.d981458441a775d647b7.js
for the specific instance in

this.preventParentScroll = e=>{
          e.preventDefault();
          const t = this.$ref && this.$ref.scrollTop + e.deltaY;
          this.scrollTo(t || 0)
        },

on macOS + touchpad i get :

deltaMode: 0
deltaX: 0
deltaY: -1
deltaZ: 0

They can probably adjust with something like:

function getWheelDeltaInPixels(e) {
  switch (e.deltaMode) {
    case WheelEvent.DOM_DELTA_PIXEL: return 1;
    case WheelEvent.DOM_DELTA_LINE:  return parseFloat(window.getComputedStyle(e.target).getPropertyValue('line-height'));
    case WheelEvent.DOM_DELTA_PAGE:  return document.scrollingElement.offsetHeight;
  }
}

courtesy of Thomas

also in https://www.redditstatic.com/desktop2x/vendors~Chat~Governance~Reddit.9987d69e1a1ddc9cab24.js
but not used in this context it seems.

          vn = Jt.extend({
            deltaX: function (e) {
              return ("deltaX" in e)
                ? e.deltaX
                : ("wheelDeltaX" in e)
                ? -e.wheelDeltaX
                : 0;
            },
            deltaY: function (e) {
              return ("deltaY" in e)
                ? e.deltaY
                : ("wheelDeltaY" in e)
                ? -e.wheelDeltaY
                : ("wheelDelta" in e)
                ? -e.wheelDelta
                : 0;
            },
            deltaZ: null,
            deltaMode: null,
          }),
Depends on: 1392460
See Also: → 1541361, 1448830, 1350616

Duping this since we believe the real issue here, and representing it on a much more broad variety of sites, is captured well in bug 1392460.

Status: NEW → RESOLVED
Closed: 4 years ago
Resolution: --- → DUPLICATE
Performance Impact: --- → ?
Whiteboard: [qf]
You need to log in before you can comment on or make changes to this bug.