Skip to content

Commit 4bec90f

Browse files
authored
fix(nextjs): Do not start a navigation if the from URL is the same (#3814)
Right now in certain cases (like with SSR), the router change state triggers twice for a single pageload. This means that a pageload will end early and start a navigation transaction with the same name. See logic: https://github.com/vercel/next.js/blob/e89b8e466aad110f8af3f60ef7d8292f6064a245/packages/next/client/index.tsx#L204 This patch adds a check to make sure that navigation transactions are only created if the route URL is different.
1 parent 14ccb55 commit 4bec90f

File tree

2 files changed

+65
-29
lines changed

2 files changed

+65
-29
lines changed

packages/nextjs/src/performance/client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ function changeStateWrapper(originalChangeStateWrapper: RouterChangeState): Wrap
8686
// internal API.
8787
...args: any[]
8888
): Promise<boolean> {
89-
if (startTransaction !== undefined) {
89+
const newTransactionName = stripUrlQueryAndFragment(url);
90+
// do not start a transaction if it's from the same page
91+
if (startTransaction !== undefined && prevTransactionName !== newTransactionName) {
9092
if (activeTransaction) {
9193
activeTransaction.finish();
9294
}
@@ -98,7 +100,7 @@ function changeStateWrapper(originalChangeStateWrapper: RouterChangeState): Wrap
98100
if (prevTransactionName) {
99101
tags.from = prevTransactionName;
100102
}
101-
prevTransactionName = stripUrlQueryAndFragment(url);
103+
prevTransactionName = newTransactionName;
102104
activeTransaction = startTransaction({
103105
name: prevTransactionName,
104106
op: 'navigation',

packages/nextjs/test/performance/client.test.ts

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ jest.mock('next/router', () => {
1919
};
2020
});
2121

22-
type Table<I = string, O = string> = Array<{ in: I; out: O }>;
23-
2422
describe('client', () => {
23+
beforeEach(() => {
24+
readyCalled = false;
25+
if (Router.router) {
26+
Router.router.changeState('pushState', '/[user]/posts/[id]', '/abhi/posts/123', {});
27+
}
28+
});
29+
2530
describe('nextRouterInstrumentation', () => {
2631
it('waits for Router.ready()', () => {
2732
const mockStartTransaction = jest.fn();
@@ -49,54 +54,83 @@ describe('client', () => {
4954
expect(mockStartTransaction).toHaveBeenCalledTimes(0);
5055
});
5156

52-
it('creates navigation transactions', () => {
53-
const mockStartTransaction = jest.fn();
54-
nextRouterInstrumentation(mockStartTransaction, false);
55-
expect(mockStartTransaction).toHaveBeenCalledTimes(0);
56-
57-
const table: Table<Array<string | unknown>, Record<string, unknown>> = [
58-
{
59-
in: ['pushState', '/posts/[id]', '/posts/32', {}],
60-
out: {
57+
describe('navigation transactions', () => {
58+
// [name, in, out]
59+
const table: Array<[string, [string, string, string, Record<string, unknown>], Record<string, unknown>]> = [
60+
[
61+
'creates parameterized transaction',
62+
['pushState', '/posts/[id]', '/posts/32', {}],
63+
{
6164
name: '/posts/[id]',
6265
op: 'navigation',
6366
tags: {
64-
from: '/posts/[id]',
67+
from: '/[user]/posts/[id]',
6568
method: 'pushState',
6669
'routing.instrumentation': 'next-router',
6770
},
6871
},
69-
},
70-
{
71-
in: ['replaceState', '/posts/[id]?name=cat', '/posts/32?name=cat', {}],
72-
out: {
72+
],
73+
[
74+
'strips query parameters',
75+
['replaceState', '/posts/[id]?name=cat', '/posts/32?name=cat', {}],
76+
{
7377
name: '/posts/[id]',
7478
op: 'navigation',
7579
tags: {
76-
from: '/posts/[id]',
80+
from: '/[user]/posts/[id]',
7781
method: 'replaceState',
7882
'routing.instrumentation': 'next-router',
7983
},
8084
},
81-
},
82-
{
83-
in: ['pushState', '/about', '/about', {}],
84-
out: {
85+
],
86+
[
87+
'creates regular transactions',
88+
['pushState', '/about', '/about', {}],
89+
{
8590
name: '/about',
8691
op: 'navigation',
8792
tags: {
88-
from: '/about',
93+
from: '/[user]/posts/[id]',
8994
method: 'pushState',
9095
'routing.instrumentation': 'next-router',
9196
},
9297
},
93-
},
98+
],
9499
];
95100

96-
table.forEach(test => {
97-
// @ts-ignore changeState can be called with array spread
98-
Router.router?.changeState(...test.in);
99-
expect(mockStartTransaction).toHaveBeenLastCalledWith(test.out);
101+
it.each(table)('%s', (...test) => {
102+
const mockStartTransaction = jest.fn();
103+
nextRouterInstrumentation(mockStartTransaction, false);
104+
expect(mockStartTransaction).toHaveBeenCalledTimes(0);
105+
106+
// @ts-ignore we can spread into test
107+
Router.router?.changeState(...test[1]);
108+
expect(mockStartTransaction).toHaveBeenLastCalledWith(test[2]);
109+
});
110+
});
111+
112+
it('does not create navigation transaction with the same name', () => {
113+
const mockStartTransaction = jest.fn();
114+
nextRouterInstrumentation(mockStartTransaction, false);
115+
expect(mockStartTransaction).toHaveBeenCalledTimes(0);
116+
117+
Router.router?.changeState('pushState', '/login', '/login', {});
118+
expect(mockStartTransaction).toHaveBeenCalledTimes(1);
119+
expect(mockStartTransaction).toHaveBeenLastCalledWith({
120+
name: '/login',
121+
op: 'navigation',
122+
tags: { from: '/[user]/posts/[id]', method: 'pushState', 'routing.instrumentation': 'next-router' },
123+
});
124+
125+
Router.router?.changeState('pushState', '/login', '/login', {});
126+
expect(mockStartTransaction).toHaveBeenCalledTimes(1);
127+
128+
Router.router?.changeState('pushState', '/posts/[id]', '/posts/123', {});
129+
expect(mockStartTransaction).toHaveBeenCalledTimes(2);
130+
expect(mockStartTransaction).toHaveBeenLastCalledWith({
131+
name: '/posts/[id]',
132+
op: 'navigation',
133+
tags: { from: '/login', method: 'pushState', 'routing.instrumentation': 'next-router' },
100134
});
101135
});
102136
});

0 commit comments

Comments
 (0)