-
Notifications
You must be signed in to change notification settings - Fork 26.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Scroll restoration bug caused by idx
reset to 0 on reload
#36861
Conversation
changed key from index to random string, to be inconsistent with session storage when reloading
Thanks for doing this work @AkifumiSato , I've been running into a related issue. Worked around my one partially by accessing As you've addressed, a unique key is needed... you can see the same approach used if you check history.state of sites like twitter and instagram. I would ask that you ideally make the |
Failing test suitesCommit: 17fa5d0
Expand output● CLI Usage › info › should print output
Read more about building and testing Next.js in contributing.md. |
Stats from current PRDefault Build (Increase detected
|
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
buildDuration | 15.2s | 15.3s | |
buildDurationCached | 6.4s | 6.4s | -16ms |
nodeModulesSize | 479 MB | 479 MB |
Page Load Tests Overall increase ✓
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
/ failed reqs | 0 | 0 | ✓ |
/ total time (seconds) | 3.751 | 3.703 | -0.05 |
/ avg req/sec | 666.54 | 675.06 | +8.52 |
/error-in-render failed reqs | 0 | 0 | ✓ |
/error-in-render total time (seconds) | 1.169 | 1.144 | -0.03 |
/error-in-render avg req/sec | 2138.71 | 2185.65 | +46.94 |
Client Bundles (main, webpack) Overall increase ⚠️
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
925.HASH.js gzip | 179 B | 179 B | ✓ |
framework-HASH.js gzip | 42 kB | 42 kB | ✓ |
main-HASH.js gzip | 29.2 kB | 29.2 kB | |
webpack-HASH.js gzip | 1.54 kB | 1.54 kB | ✓ |
Overall change | 72.9 kB | 73 kB |
Legacy Client Bundles (polyfills)
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
polyfills-HASH.js gzip | 31 kB | 31 kB | ✓ |
Overall change | 31 kB | 31 kB | ✓ |
Client Pages
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
_app-HASH.js gzip | 1.36 kB | 1.36 kB | ✓ |
_error-HASH.js gzip | 193 B | 193 B | ✓ |
amp-HASH.js gzip | 308 B | 308 B | ✓ |
css-HASH.js gzip | 327 B | 327 B | ✓ |
dynamic-HASH.js gzip | 2.71 kB | 2.71 kB | ✓ |
head-HASH.js gzip | 359 B | 359 B | ✓ |
hooks-HASH.js gzip | 920 B | 920 B | ✓ |
image-HASH.js gzip | 5.73 kB | 5.73 kB | ✓ |
index-HASH.js gzip | 263 B | 263 B | ✓ |
link-HASH.js gzip | 2.65 kB | 2.65 kB | ✓ |
routerDirect..HASH.js gzip | 320 B | 320 B | ✓ |
script-HASH.js gzip | 391 B | 391 B | ✓ |
withRouter-HASH.js gzip | 318 B | 318 B | ✓ |
85e02e95b279..7e3.css gzip | 107 B | 107 B | ✓ |
Overall change | 16 kB | 16 kB | ✓ |
Client Build Manifests
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
_buildManifest.js gzip | 458 B | 458 B | ✓ |
Overall change | 458 B | 458 B | ✓ |
Rendered Page Sizes
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
index.html gzip | 533 B | 533 B | ✓ |
link.html gzip | 547 B | 547 B | ✓ |
withRouter.html gzip | 528 B | 528 B | ✓ |
Overall change | 1.61 kB | 1.61 kB | ✓ |
Diffs
Diff for main-HASH.js
@@ -4533,6 +4533,7 @@
exports.isLocalURL = isLocalURL;
exports.interpolateAs = interpolateAs;
exports.resolveHref = resolveHref;
+ exports.createKey = createKey;
exports["default"] = void 0;
var _normalizeTrailingSlash = __webpack_require__(2700);
var _routeLoader = __webpack_require__(2497);
@@ -4931,6 +4932,11 @@
throw err;
}));
}
+ function createKey() {
+ return Math.random()
+ .toString(36)
+ .slice(2, 10);
+ }
var Router = /*#__PURE__*/ (function() {
function Router(pathname1, query1, as1, param) {
var initialProps = param.initialProps,
@@ -4955,7 +4961,7 @@
this.sdr = {};
// In-flight middleware preflight requests
this.sde = {};
- this._idx = 0;
+ this._key = createKey();
this.onPopState = function(e) {
var state = e.state;
if (!state) {
@@ -4987,11 +4993,11 @@
var url = state.url,
as = state.as,
options = state.options,
- idx = state.idx;
+ key = state.key;
if (false) {
var v;
}
- _this._idx = idx;
+ _this._key = key;
var pathname2 = (0, _parseRelativeUrl).parseRelativeUrl(url)
.pathname;
// Make sure we don't re-render on initial load,
@@ -5947,8 +5953,8 @@
as: as,
options: options,
__N: true,
- idx: (this._idx =
- method !== "pushState" ? this._idx : this._idx + 1)
+ key: (this._key =
+ method !== "pushState" ? this._key : createKey())
}, // Passing the empty string here should be safe against future changes to the method.
// https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
"",
Diff for index.html
@@ -19,7 +19,7 @@
defer=""
></script>
<script
- src="/_next/static/chunks/main-06bbb6769d0ae4b4.js"
+ src="/_next/static/chunks/main-dc9db7a6be528c25.js"
defer=""
></script>
<script
Diff for link.html
@@ -19,7 +19,7 @@
defer=""
></script>
<script
- src="/_next/static/chunks/main-06bbb6769d0ae4b4.js"
+ src="/_next/static/chunks/main-dc9db7a6be528c25.js"
defer=""
></script>
<script
Diff for withRouter.html
@@ -19,7 +19,7 @@
defer=""
></script>
<script
- src="/_next/static/chunks/main-06bbb6769d0ae4b4.js"
+ src="/_next/static/chunks/main-dc9db7a6be528c25.js"
defer=""
></script>
<script
Default Build with SWC (Increase detected ⚠️ )
General Overall increase ⚠️
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
buildDuration | 17.6s | 17.5s | -155ms |
buildDurationCached | 6.3s | 6.4s | |
nodeModulesSize | 479 MB | 479 MB |
Page Load Tests Overall decrease ⚠️
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
/ failed reqs | 0 | 0 | ✓ |
/ total time (seconds) | 3.717 | 3.794 | |
/ avg req/sec | 672.67 | 658.88 | |
/error-in-render failed reqs | 0 | 0 | ✓ |
/error-in-render total time (seconds) | 1.122 | 1.171 | |
/error-in-render avg req/sec | 2228.89 | 2135.23 |
Client Bundles (main, webpack) Overall increase ⚠️
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
925.HASH.js gzip | 178 B | 178 B | ✓ |
framework-HASH.js gzip | 42.7 kB | 42.7 kB | ✓ |
main-HASH.js gzip | 29.6 kB | 29.7 kB | |
webpack-HASH.js gzip | 1.54 kB | 1.54 kB | ✓ |
Overall change | 74 kB | 74.1 kB |
Legacy Client Bundles (polyfills)
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
polyfills-HASH.js gzip | 31 kB | 31 kB | ✓ |
Overall change | 31 kB | 31 kB | ✓ |
Client Pages
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
_app-HASH.js gzip | 1.35 kB | 1.35 kB | ✓ |
_error-HASH.js gzip | 179 B | 179 B | ✓ |
amp-HASH.js gzip | 311 B | 311 B | ✓ |
css-HASH.js gzip | 324 B | 324 B | ✓ |
dynamic-HASH.js gzip | 2.89 kB | 2.89 kB | ✓ |
head-HASH.js gzip | 357 B | 357 B | ✓ |
hooks-HASH.js gzip | 920 B | 920 B | ✓ |
image-HASH.js gzip | 5.82 kB | 5.82 kB | ✓ |
index-HASH.js gzip | 261 B | 261 B | ✓ |
link-HASH.js gzip | 2.78 kB | 2.78 kB | ✓ |
routerDirect..HASH.js gzip | 322 B | 322 B | ✓ |
script-HASH.js gzip | 392 B | 392 B | ✓ |
withRouter-HASH.js gzip | 317 B | 317 B | ✓ |
85e02e95b279..7e3.css gzip | 107 B | 107 B | ✓ |
Overall change | 16.3 kB | 16.3 kB | ✓ |
Client Build Manifests
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
_buildManifest.js gzip | 457 B | 457 B | ✓ |
Overall change | 457 B | 457 B | ✓ |
Rendered Page Sizes Overall decrease ✓
vercel/next.js canary | AkifumiSato/next.js fix-scroll-restoration | Change | |
---|---|---|---|
index.html gzip | 532 B | 531 B | -1 B |
link.html gzip | 547 B | 546 B | -1 B |
withRouter.html gzip | 529 B | 527 B | -2 B |
Overall change | 1.61 kB | 1.6 kB | -4 B |
Diffs
Diff for main-HASH.js
@@ -4533,6 +4533,7 @@
exports.isLocalURL = isLocalURL;
exports.interpolateAs = interpolateAs;
exports.resolveHref = resolveHref;
+ exports.createKey = createKey;
exports["default"] = void 0;
var _normalizeTrailingSlash = __webpack_require__(2700);
var _routeLoader = __webpack_require__(2497);
@@ -4931,6 +4932,11 @@
throw err;
}));
}
+ function createKey() {
+ return Math.random()
+ .toString(36)
+ .slice(2, 10);
+ }
var Router = /*#__PURE__*/ (function() {
function Router(pathname1, query1, as1, param) {
var initialProps = param.initialProps,
@@ -4955,7 +4961,7 @@
this.sdr = {};
// In-flight middleware preflight requests
this.sde = {};
- this._idx = 0;
+ this._key = createKey();
this.onPopState = function(e) {
var state = e.state;
if (!state) {
@@ -4987,11 +4993,11 @@
var url = state.url,
as = state.as,
options = state.options,
- idx = state.idx;
+ key = state.key;
if (false) {
var v;
}
- _this._idx = idx;
+ _this._key = key;
var pathname2 = (0, _parseRelativeUrl).parseRelativeUrl(url)
.pathname;
// Make sure we don't re-render on initial load,
@@ -5947,8 +5953,8 @@
as: as,
options: options,
__N: true,
- idx: (this._idx =
- method !== "pushState" ? this._idx : this._idx + 1)
+ key: (this._key =
+ method !== "pushState" ? this._key : createKey())
}, // Passing the empty string here should be safe against future changes to the method.
// https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
"",
Diff for index.html
@@ -19,7 +19,7 @@
defer=""
></script>
<script
- src="/_next/static/chunks/main-06bbb6769d0ae4b4.js"
+ src="/_next/static/chunks/main-dc9db7a6be528c25.js"
defer=""
></script>
<script
Diff for link.html
@@ -19,7 +19,7 @@
defer=""
></script>
<script
- src="/_next/static/chunks/main-06bbb6769d0ae4b4.js"
+ src="/_next/static/chunks/main-dc9db7a6be528c25.js"
defer=""
></script>
<script
Diff for withRouter.html
@@ -19,7 +19,7 @@
defer=""
></script>
<script
- src="/_next/static/chunks/main-06bbb6769d0ae4b4.js"
+ src="/_next/static/chunks/main-dc9db7a6be528c25.js"
defer=""
></script>
<script
I wanted to add a little more detail of why I believe it's important to expose Window scroll position is only a single restore state to factor in, but there can be things like carousel horizontal scroll positions inside of a page, or a ui component that's been expanded and then a link clicked. These are states that are not ideally stored as query parameters, but do need to be unique to the history position as the same page may be seen multiple times in history and have different internal restore state. |
@mikestead Thanks for your comment! |
The test is failing, but it appears to be unrelated to my fix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
@mikestead New pull request created. |
If
experimental.scrollRestoration = true
, reloading resets the value of Router's_idx
to 0, so the scroll position cannot be restored correctly between pages.Since this is stored in session storage, the scroll position may be restored with incorrect values.
/person/1
/person/2
/person/2
This modification restores the position of the browser back after reloading.
At the end of the first video, the scroll is not restored on browser back after reloading and is set to 0.
In the second video, the position is restored correctly.
before.mov
after.mov
I made a similar correction in the following PR, but I had to make a new one because I had included other correction.
#35770
Bug
fixes #number
contributing.md
Feature
fixes #number
contributing.md
Documentation / Examples
yarn lint