Slow scrolling on Reddit chat
Categories
(Web Compatibility :: Site Reports, defect)
Tracking
(Performance Impact:?)
Performance Impact | ? |
People
(Reporter: miketaylr, Unassigned)
References
()
Details
Originally reported @ https://github.com/webcompat/web-bugs/issues/51383
STR:
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
Comment 1•4 years ago
|
||
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
Comment 2•4 years ago
|
||
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.
![]() |
||
Comment 3•4 years ago
|
||
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'
}), '↓')))
}
}
Comment 4•4 years ago
|
||
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.
![]() |
||
Comment 5•4 years ago
|
||
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,
}),
![]() |
||
Updated•4 years ago
|
Comment 6•4 years ago
|
||
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.
Updated•2 years ago
|
Description
•