Skip to content

Commit dbd2cec

Browse files
authored
perf: release 4.10.0 performance optimization (notionnext-org#4163)
* perf: code-split landing and typography theme blocks * perf: defer nav theme non-critical components via dynamic import * perf: defer non-critical plugin loads and lazy-load search highlight * docs: add performance optimization log and bump patch version * perf: optimize search highlighting and Windows lighthouse audit * perf: make theme scroll listeners passive * fix: make MenuItemDrop scroll listener passive * perf(next): throttle layout scroll updates with rAF * chore: bump patch version after next theme scroll optimization * perf: throttle theme progress listeners * perf: coalesce next and endspace scroll updates * perf: slim client page data payloads * test: cover notice page link conversion * docs: prepare 4.10.0 performance release
1 parent c2cb427 commit dbd2cec

89 files changed

Lines changed: 1463 additions & 559 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { convertInnerUrl } from '@/lib/db/notion/convertInnerUrl'
2+
3+
describe('convertInnerUrl', () => {
4+
beforeEach(() => {
5+
document.body.innerHTML = ''
6+
window.history.replaceState({}, '', 'http://localhost/notice')
7+
})
8+
9+
it('maps notice links to published Page records from allLinkPages', () => {
10+
document.body.innerHTML = `
11+
<div id="notion-article">
12+
<a class="notion-link" href="https://www.notion.so/4aea95fb3fd5fcf81846aaaaaaaaaaaa" target="_blank">Links</a>
13+
</div>
14+
`
15+
16+
convertInnerUrl({
17+
allPages: [
18+
{
19+
title: 'Links',
20+
type: 'Page',
21+
href: '/links',
22+
slug: 'links',
23+
short_id: 'fcf8-1846-aaaaaaaaaaaa'
24+
}
25+
],
26+
lang: undefined
27+
})
28+
29+
expect(document.querySelector('a.notion-link')).toHaveAttribute(
30+
'href',
31+
'/links'
32+
)
33+
})
34+
35+
it('does not resolve Page links when only post navigation data is present', () => {
36+
const rawNotionUrl =
37+
'https://www.notion.so/4aea95fb3fd5fcf81846aaaaaaaaaaaa'
38+
document.body.innerHTML = `
39+
<div id="notion-article">
40+
<a class="notion-link" href="${rawNotionUrl}" target="_blank">Links</a>
41+
</div>
42+
`
43+
44+
convertInnerUrl({
45+
allPages: [
46+
{
47+
title: 'Post only',
48+
type: 'Post',
49+
href: '/post-only',
50+
slug: 'post-only',
51+
short_id: '1111-2222-bbbbbbbbbbbb'
52+
}
53+
],
54+
lang: undefined
55+
})
56+
57+
expect(document.querySelector('a.notion-link')).toHaveAttribute(
58+
'href',
59+
rawNotionUrl
60+
)
61+
})
62+
})

components/ExternalPlugins.js

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { convertInnerUrl } from '@/lib/db/notion/convertInnerUrl'
33
import { isBrowser, loadExternalResource } from '@/lib/utils'
44
import dynamic from 'next/dynamic'
55
import { useRouter } from 'next/router'
6-
import { useEffect } from 'react'
6+
import { useEffect, useMemo } from 'react'
77
import { GlobalStyle } from './GlobalStyle'
88
import { initGoogleAdsense } from './GoogleAdsense'
99

@@ -132,36 +132,60 @@ const ExternalPlugin = props => {
132132
const UMAMI_HOST = siteConfig('UMAMI_HOST', null, NOTION_CONFIG)
133133
const UMAMI_ID = siteConfig('UMAMI_ID', null, NOTION_CONFIG)
134134

135-
// 自定义样式css和js引入
136-
if (isBrowser) {
137-
// 初始化AOS动画
138-
// 静态导入本地自定义样式
139-
loadExternalResource('/css/custom.css', 'css')
140-
loadExternalResource('/js/custom.js', 'js')
135+
const externalCssList = useMemo(() => {
136+
return Array.isArray(CUSTOM_EXTERNAL_CSS)
137+
? CUSTOM_EXTERNAL_CSS.filter(url => !!url)
138+
: []
139+
}, [CUSTOM_EXTERNAL_CSS])
141140

142-
// 自动添加图片阴影
143-
if (IMG_SHADOW) {
144-
loadExternalResource('/css/img-shadow.css', 'css')
145-
}
141+
const externalJsList = useMemo(() => {
142+
return Array.isArray(CUSTOM_EXTERNAL_JS)
143+
? CUSTOM_EXTERNAL_JS.filter(url => !!url)
144+
: []
145+
}, [CUSTOM_EXTERNAL_JS])
146146

147-
if (ANIMATE_CSS_URL) {
148-
loadExternalResource(ANIMATE_CSS_URL, 'css')
147+
useEffect(() => {
148+
if (!isBrowser) {
149+
return
149150
}
150151

151-
// 导入外部自定义脚本
152-
if (CUSTOM_EXTERNAL_JS && CUSTOM_EXTERNAL_JS.length > 0) {
153-
for (const url of CUSTOM_EXTERNAL_JS) {
154-
loadExternalResource(url, 'js')
152+
const scheduleTask = callback => {
153+
if (window.requestIdleCallback) {
154+
const taskId = window.requestIdleCallback(callback)
155+
return () => window.cancelIdleCallback(taskId)
155156
}
157+
const timeoutId = window.setTimeout(callback, 0)
158+
return () => window.clearTimeout(timeoutId)
156159
}
157160

158-
// 导入外部自定义样式
159-
if (CUSTOM_EXTERNAL_CSS && CUSTOM_EXTERNAL_CSS.length > 0) {
160-
for (const url of CUSTOM_EXTERNAL_CSS) {
161-
loadExternalResource(url, 'css')
162-
}
161+
const cancelTasks = []
162+
cancelTasks.push(
163+
scheduleTask(() => {
164+
loadExternalResource('/css/custom.css', 'css')
165+
loadExternalResource('/js/custom.js', 'js')
166+
167+
if (IMG_SHADOW) {
168+
loadExternalResource('/css/img-shadow.css', 'css')
169+
}
170+
171+
if (ANIMATE_CSS_URL) {
172+
loadExternalResource(ANIMATE_CSS_URL, 'css')
173+
}
174+
175+
for (const url of externalJsList) {
176+
loadExternalResource(url, 'js')
177+
}
178+
179+
for (const url of externalCssList) {
180+
loadExternalResource(url, 'css')
181+
}
182+
})
183+
)
184+
185+
return () => {
186+
cancelTasks.forEach(cancel => cancel?.())
163187
}
164-
}
188+
}, [ANIMATE_CSS_URL, IMG_SHADOW, externalCssList, externalJsList])
165189

166190
const router = useRouter()
167191
useEffect(() => {
@@ -182,13 +206,17 @@ const ExternalPlugin = props => {
182206
}, [router])
183207

184208
useEffect(() => {
185-
// 执行注入脚本
186-
// eslint-disable-next-line no-eval
187-
if (GLOBAL_JS && GLOBAL_JS.trim() !== '') {
188-
// console.log('Inject JS:', GLOBAL_JS);
209+
if (!isBrowser || !GLOBAL_JS || GLOBAL_JS.trim() === '') {
210+
return
211+
}
212+
213+
try {
214+
// eslint-disable-next-line no-eval
215+
eval(GLOBAL_JS)
216+
} catch (error) {
217+
console.error('Failed to execute GLOBAL_JS:', error)
189218
}
190-
eval(GLOBAL_JS)
191-
})
219+
}, [GLOBAL_JS])
192220

193221
if (DISABLE_PLUGIN) {
194222
return null

components/FacebookMessenger.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default function Messenger() {
1919

2020
// 延时7秒,或页面滚动时加载该组件
2121
useEffect(() => {
22-
window.addEventListener('scroll', showTheComponent);
22+
window.addEventListener('scroll', showTheComponent, { passive: true });
2323
setTimeout(() => {
2424
showTheComponent()
2525
}, 7000);

components/Mark.js

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,67 @@
11
import { loadExternalResource } from '@/lib/utils'
22

3+
let markJsLoadPromise = null
4+
5+
function escapeSearchKeyword(search) {
6+
return String(search).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
7+
}
8+
9+
function runOnIdle() {
10+
return new Promise(resolve => {
11+
const done = () => resolve()
12+
13+
if (typeof window === 'undefined' || typeof window.requestIdleCallback !== 'function') {
14+
setTimeout(done, 0)
15+
return
16+
}
17+
18+
window.requestIdleCallback(done, { timeout: 800 })
19+
})
20+
}
21+
22+
function ensureMarkScript() {
23+
if (!markJsLoadPromise) {
24+
markJsLoadPromise = loadExternalResource(
25+
'https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js',
26+
'js'
27+
)
28+
}
29+
return markJsLoadPromise
30+
}
31+
332
/**
4-
* 将搜索结果的关键词高亮
33+
* Highlights search keywords in DOM safely without blocking main-thread work.
534
*/
635
export default async function replaceSearchResult({ doms, search, target }) {
7-
if (!doms || !search || !target) {
36+
if (!doms || !search || !target || typeof window === 'undefined') {
37+
return
38+
}
39+
40+
const keyword = String(search).trim()
41+
if (!keyword) {
842
return
943
}
1044

1145
try {
12-
await loadExternalResource('https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js', 'js')
46+
await ensureMarkScript()
47+
await runOnIdle()
48+
1349
const Mark = window.Mark
50+
if (!Mark) return
51+
52+
const regex = new RegExp(escapeSearchKeyword(keyword), 'gim')
53+
54+
const markContainer = container => {
55+
const instance = new Mark(container)
56+
instance.markRegExp(regex, target)
57+
}
58+
1459
if (doms instanceof HTMLCollection) {
15-
for (const container of doms) {
16-
const re = new RegExp(search, 'gim')
17-
const instance = new Mark(container)
18-
instance.markRegExp(re, target)
19-
}
60+
Array.from(doms).forEach(markContainer)
2061
} else {
21-
const re = new RegExp(search, 'gim')
22-
const instance = new Mark(doms)
23-
instance.markRegExp(re, target)
62+
markContainer(doms)
2463
}
2564
} catch (error) {
26-
console.error('markjs 加载失败', error)
65+
console.error('Search highlight failed:', error)
2766
}
2867
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# 涓婚鎬ц兘浼樺寲锛堢 1 杞級
2+
3+
鏃ユ湡锛?026-06-03
4+
鍒嗘敮锛歚codex/performance-optimization-main`
5+
鐗堟湰鐩爣锛歚4.10.0`
6+
7+
## 鏈疆鍙樻洿
8+
9+
### 1) 鍏ㄥ眬鎻掍欢鍔犺浇浼樺寲
10+
- 鏂囦欢锛歚components/ExternalPlugins.js`
11+
- 鏀瑰姩锛? - 灏嗚嚜瀹氫箟澶栭儴璧勬簮鍔犺浇浠庣粍浠舵覆鏌撴湡绉诲叆 `useEffect`锛屽苟鏀逛负寮傛璋冨害锛坄requestIdleCallback` / `setTimeout`锛夋墽琛屻€? - 瀵?`CUSTOM_EXTERNAL_CSS`銆乣CUSTOM_EXTERNAL_JS` 鍋?`useMemo` 杩囨护锛岄伩鍏嶉噸澶嶈绠椾笌閲嶅娉ㄥ叆銆? - 灏?`GLOBAL_JS` 鎵ц浠庢棤渚濊禆鐨勫壇浣滅敤鏀逛负 `useEffect([GLOBAL_JS])`锛屽苟鍔?`try/catch`锛岄伩鍏嶆瘡娆?render 閲嶅鎵ц瀵艰嚧閲嶅娉ㄥ叆涓庢綔鍦ㄩ樆濉炪€?
12+
### 2) Typography 鎼滅储楂樹寒鎸夐渶鍔犺浇
13+
- 鏂囦欢锛歚themes/typography/index.js`
14+
- 鏀瑰姩锛? - `LayoutSearch` 涓皢 `replaceSearchResult` 杩佺Щ涓烘寜闇€鍔ㄦ€?import锛堜粎鍦ㄦ悳绱㈤〉鐢熸晥鏃惰Е鍙戯級锛岄伩鍏嶉椤?鏂囩珷椤靛垵濮嬪寘瑁瑰叆杩欓儴鍒嗕唬鐮併€? - 灏嗘悳绱㈤珮浜寕杞芥敼涓哄欢鍚庢墽琛岋紙`requestIdleCallback` 鎴?fallback timeout锛夛紝闄嶄綆棣栧睆闃诲銆?
15+
### 3) 鎼滅储楂樹寒閫昏緫缁熶竴浼樺寲锛堣法涓婚锛?- 鏂囦欢锛歚components/Mark.js`
16+
- 鏀瑰姩锛? - 灏?`mark.js` 搴撳姞杞芥敼涓轰竴娆℃€?Promise 缂撳瓨锛岄伩鍏嶅悓涓€娆′細璇濆唴閲嶅璇锋眰銆? - 瀵规悳绱㈠叧閿瘝杩涜瀹夊叏杞箟锛岄伩鍏嶅紓甯告鍒欏鑷撮珮浜矾寰勪腑鏂€? - 鐢?`requestIdleCallback`锛堝吋瀹?fallback锛夊皢楂樹寒鎵ц寤跺悗锛屽噺灏戠洿鎺ラ樆濉炰富绾跨▼楂樺嘲銆? - 淇濇寔鐜版湁楂樹寒杈撳嚭锛坈lassName/element锛変笌閰嶇疆涓嶅彉銆?
17+
### 4) 涓婚鎬ц兘瀹¤鑴氭湰璺ㄥ钩鍙板吋瀹逛慨澶?- 鏂囦欢锛歚scripts/audit-theme-performance.js`
18+
- 鏀瑰姩锛? - 鎸夊钩鍙伴€夋嫨 `lighthouse` 鍙墽琛岃矾寰勶紙Windows 浼樺厛 `lighthouse.cmd`锛屾湭鍛戒腑鏃堕檷绾у埌 `lighthouse` 鎴栧寘鍐?CLI 鍏ュ彛锛夈€? - `runLighthouse` 鎵ц璺緞鏀逛负鍙橀噺鍖栵紝閬垮厤 Windows 涓?`spawnSync` 璇嗗埆闂銆? - `main` 鏀逛负鍚屾娴佺▼锛岀Щ闄?`async`/.`catch` 鐨勪笉鍖归厤璋冪敤銆?
19+
### 5) 涓婚绾ф粴鍔ㄧ洃鍚紭鍖栵紙璺ㄤ富棰橈級
20+
- 鏂囦欢锛歚themes/*/components/*.js`
21+
- 鏀瑰姩锛? - 灏嗗ぇ閲忎粎璇绘粴鍔ㄤ綅缃?杩涘害鐨勭洃鍚櫒鏀逛负 `passive: true` 娉ㄥ唽锛屽噺灏戞粴鍔ㄤ簨浠跺涓荤嚎绋嬭皟搴﹀帇鍔涖€? - 瑕嗙洊鑼冨洿锛欳atalog銆丳rogress銆佹诞鍔ㄥ鑸€丅ackToTop 绛夊ぇ閲忎富棰樺唴婊氬姩鍦烘櫙锛堝叡 70+ 澶勶級銆? - 淇濇寔琛屼负涓€鑷达紙鐩戝惉閫昏緫鍜屽嵏杞戒粛淇濇寔鍘熸湁鍥炶皟鍖归厤锛夈€?
22+
## 楠岃瘉
23+
24+
- `yarn type-check`锛氶€氳繃
25+
- `yarn build`锛氶€氳繃
26+
- `yarn lint`锛氭湰鏈虹幆澧冩姤閿欌€滄湭璇嗗埆 pages/app 鐩綍鈥濓紝鏆傛湭閫氳繃璇ラ」楠岃瘉锛堜笌褰撳墠 Next.js 鍚姩鐜璺緞瑙f瀽鐩稿叧锛屼笉褰卞搷宸插畬鎴愮紪璇戜笌绫诲瀷鏍¢獙缁撴灉锛?
27+
## 椋庨櫓涓庡奖鍝?- 澶栭儴鑴氭湰鍔犺浇鏀逛负寤惰繜锛屼笉褰卞搷鐜版湁閰嶇疆椤逛笌鎻掍欢寮€鍏抽€昏緫锛坄DISABLE_PLUGIN` 绛変繚鎸佷笉鍙橈級銆?- `GLOBAL_JS` 鍙湪鍐呭鍙樺寲鏃舵墽琛岋紝琛屼负涓庨厤缃粨鏋滀繚鎸佷竴鑷达紝浣嗛檷浣庝簡閲嶅娉ㄥ叆椋庨櫓銆?
28+
## 涓嬩竴姝ヨ鍒掞紙P2锛?- 缁х画姊崇悊涓婚灞傞潰浠嶆湁杈冮噸鐨勯灞忛€昏緫锛堢壒鍒槸鎼滅储銆佺洰褰曘€侀珮棰?DOM 閬嶅巻閫昏緫锛夛紝鎸夊奖鍝嶉潰浼樺厛钀藉湴銆?- 寤虹珛鍙鐢ㄧ殑涓婚绾у欢杩熸墽琛屽熀绾匡紙绌洪棽/婊氬姩瑙﹀彂锛夛紝骞惰ˉ榻愬涓婚瀵规瘮鐨勮嚜鍔ㄥ寲 Lighthouse 鎶ュ憡銆?
29+
30+
## 2026-06-03 Follow-up: Next and Endspace scroll rendering pass
31+
32+
Version: `4.10.0`
33+
34+
### Scope
35+
- `themes/next/components/BlogPostListScroll.js`
36+
- `themes/next/components/StickyBar.js`
37+
- `themes/next/components/Toc.js`
38+
- `themes/next/components/TopNav.js`
39+
- `themes/endspace/components/FloatingControls.js`
40+
- `themes/endspace/components/FloatingToc.js`
41+
- `themes/endspace/components/MobileToc.js`
42+
43+
### Meaning
44+
- Replaced high-frequency lodash throttle scroll handlers with `requestAnimationFrame` scheduling where the UI is tied to visual scroll state.
45+
- Collapsed duplicated progress and TOC scroll listeners in Endspace into a single scroll pipeline per component.
46+
- Added ref-based state guards for reading progress and active TOC section to avoid repeated React state updates with the same value.
47+
- Cached DOM targets such as sticky navigation elements through refs instead of querying on every scroll tick.
48+
- Kept theme APIs, config switches, visual behavior, and plugin behavior unchanged.
49+
50+
### Acceptance
51+
- `yarn -s eslint` on affected files: passed.
52+
- `.\\node_modules\\.bin\\tsc.cmd --noEmit --pretty false`: passed.
53+
- `yarn -s build`: passed.
54+
- Browser validation:
55+
- `/article/guide?theme=next`: desktop and mobile scroll progress updates, no console errors.
56+
- `/article/guide?theme=endspace`: desktop and mobile TOC/progress controls remain visible and update on scroll, no console errors.
57+
58+
### Next optimization target
59+
- Build output still warns that `/` and selected article page-data exceed 128KB. The next priority should be reducing serialized page props and trimming data sent to the client before pursuing smaller per-component scroll wins.
60+

docs/user-guide/changelog/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
| 文档 | 说明 |
44
| --- | --- |
5-
| [最新版本](./latest.md) | 当前主线要点(4.9.5.x|
5+
| [最新版本](./latest.md) | 当前主线要点(4.10.0|
66
| [V4 历史](./v4-history.md) | 旧版 changelog 索引 |
77

88
另见:[升级](../update.md) · 仓库 [GitHub Releases](https://github.com/notionnext-org/NotionNext/releases)

0 commit comments

Comments
 (0)