perf(editor): reduce long-document decoration work and fix fold navigation

This commit is contained in:
2026-09-06 14:30:56 +08:00
parent 750e17212e
commit 874e916106
23 changed files with 9398 additions and 54 deletions
+2
View File
@@ -95,3 +95,5 @@
- 文档中的“计划实现”和“已经实现”必须明确区分;实现状态以代码、测试和运行时契约为准。
- [Markdown 语法预设与外部文件刷新](development/Markdown语法预设与外部文件刷新.md)
- [长文渲染优化与压测报告](development/长文渲染优化与压测报告.md)
@@ -0,0 +1,380 @@
[
{
"requestedHan": 25000,
"hanCharacters": 25632,
"sourceCharacters": 35728,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 379.09999999403954,
"domNodes": 2110,
"selectionMs": {
"median": 0.4000000059604645,
"p95": 0.7000000178813934,
"max": 1.300000011920929
},
"insertMs": {
"median": 6.300000011920929,
"p95": 9.599999994039536,
"max": 10.400000005960464
},
"foldMs": {
"median": 42.900000005960464,
"p95": 45.599999994039536,
"max": 45.599999994039536
},
"integrity": true,
"previewMs": [
176.30000001192093,
22.69999998807907,
19.80000001192093
],
"previewNodes": 1058,
"longTasks": {
"count": 4,
"median": 91,
"p95": 359,
"max": 359
},
"heapUsedBytes": 36560189,
"repeat": 1
},
{
"requestedHan": 25000,
"hanCharacters": 25632,
"sourceCharacters": 35728,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 218.59999999403954,
"domNodes": 2110,
"selectionMs": {
"median": 0.29999998211860657,
"p95": 0.5,
"max": 0.5
},
"insertMs": {
"median": 4.799999982118607,
"p95": 6.9000000059604645,
"max": 7.699999988079071
},
"foldMs": {
"median": 45.30000001192093,
"p95": 79,
"max": 79
},
"integrity": true,
"previewMs": [
112.90000000596046,
27.80000001192093,
25.399999976158142
],
"previewNodes": 1058,
"longTasks": {
"count": 5,
"median": 65,
"p95": 198,
"max": 198
},
"heapUsedBytes": 78394355,
"repeat": 2
},
{
"requestedHan": 25000,
"hanCharacters": 25632,
"sourceCharacters": 35728,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 207.59999999403954,
"domNodes": 2110,
"selectionMs": {
"median": 0.4000000059604645,
"p95": 0.5999999940395355,
"max": 0.699999988079071
},
"insertMs": {
"median": 4.600000023841858,
"p95": 5.9000000059604645,
"max": 6.800000011920929
},
"foldMs": {
"median": 35.900000005960464,
"p95": 45.400000005960464,
"max": 45.400000005960464
},
"integrity": true,
"previewMs": [
98.30000001192093,
29.69999998807907,
29.80000001192093
],
"previewNodes": 1058,
"longTasks": {
"count": 4,
"median": 59,
"p95": 184,
"max": 184
},
"heapUsedBytes": 72765231,
"repeat": 3
},
{
"requestedHan": 60000,
"hanCharacters": 61172,
"sourceCharacters": 85716,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 394.30000001192093,
"domNodes": 4720,
"selectionMs": {
"median": 0.4000000059604645,
"p95": 0.6000000238418579,
"max": 0.699999988079071
},
"insertMs": {
"median": 6.300000011920929,
"p95": 7.799999982118607,
"max": 13.599999994039536
},
"foldMs": {
"median": 95.80000001192093,
"p95": 145.5,
"max": 145.5
},
"integrity": true,
"previewMs": [
180.59999999403954,
61.20000001788139,
50
],
"previewNodes": 2560,
"longTasks": {
"count": 10,
"median": 103,
"p95": 358,
"max": 358
},
"heapUsedBytes": 73234300,
"repeat": 1
},
{
"requestedHan": 60000,
"hanCharacters": 61172,
"sourceCharacters": 85716,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 447.5,
"domNodes": 4720,
"selectionMs": {
"median": 0.4000000059604645,
"p95": 0.5999999940395355,
"max": 0.5999999940395355
},
"insertMs": {
"median": 6.699999988079071,
"p95": 12.699999988079071,
"max": 14.5
},
"foldMs": {
"median": 99.5,
"p95": 110.40000000596046,
"max": 110.40000000596046
},
"integrity": true,
"previewMs": [
153,
44.900000005960464,
49.400000005960464
],
"previewNodes": 2560,
"longTasks": {
"count": 9,
"median": 113,
"p95": 410,
"max": 410
},
"heapUsedBytes": 97026266,
"repeat": 2
},
{
"requestedHan": 60000,
"hanCharacters": 61172,
"sourceCharacters": 85716,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 492.69999998807907,
"domNodes": 4720,
"selectionMs": {
"median": 0.5,
"p95": 0.6000000238418579,
"max": 1
},
"insertMs": {
"median": 7.199999988079071,
"p95": 9.599999994039536,
"max": 10.100000023841858
},
"foldMs": {
"median": 115.80000001192093,
"p95": 149.7000000178814,
"max": 149.7000000178814
},
"integrity": true,
"previewMs": [
141.39999997615814,
67.5,
61.900000005960464
],
"previewNodes": 2560,
"longTasks": {
"count": 11,
"median": 109,
"p95": 445,
"max": 445
},
"heapUsedBytes": 88746335,
"repeat": 3
},
{
"requestedHan": 120000,
"hanCharacters": 122322,
"sourceCharacters": 171613,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 893.4000000059605,
"domNodes": 9139,
"selectionMs": {
"median": 0.7000000178813934,
"p95": 1.2000000178813934,
"max": 2.399999976158142
},
"insertMs": {
"median": 18,
"p95": 28.900000005960464,
"max": 31.5
},
"foldMs": {
"median": 217,
"p95": 292.90000000596046,
"max": 292.90000000596046
},
"integrity": true,
"previewMs": [
203.10000002384186,
98.7999999821186,
92.30000001192093
],
"previewNodes": 5117,
"longTasks": {
"count": 13,
"median": 199,
"p95": 796,
"max": 796
},
"heapUsedBytes": 113890741,
"repeat": 1
},
{
"requestedHan": 120000,
"hanCharacters": 122322,
"sourceCharacters": 171613,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 869.0999999940395,
"domNodes": 9139,
"selectionMs": {
"median": 0.7000000178813934,
"p95": 1.300000011920929,
"max": 3.4000000059604645
},
"insertMs": {
"median": 16.69999998807907,
"p95": 22.599999994039536,
"max": 22.80000001192093
},
"foldMs": {
"median": 220.5,
"p95": 263.09999999403954,
"max": 263.09999999403954
},
"integrity": true,
"previewMs": [
207.39999997615814,
98.7000000178814,
90.2999999821186
],
"previewNodes": 5117,
"longTasks": {
"count": 14,
"median": 205,
"p95": 780,
"max": 780
},
"heapUsedBytes": 125636820,
"repeat": 2
},
{
"requestedHan": 120000,
"hanCharacters": 122322,
"sourceCharacters": 171613,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 845.9000000059605,
"domNodes": 9139,
"selectionMs": {
"median": 0.699999988079071,
"p95": 1.0999999940395355,
"max": 2.9000000059604645
},
"insertMs": {
"median": 18.69999998807907,
"p95": 27.19999998807907,
"max": 35
},
"foldMs": {
"median": 205.7000000178814,
"p95": 263.90000000596046,
"max": 263.90000000596046
},
"integrity": true,
"previewMs": [
213.09999999403954,
100,
91.5
],
"previewNodes": 5117,
"longTasks": {
"count": 13,
"median": 191,
"p95": 768,
"max": 768
},
"heapUsedBytes": 86940861,
"repeat": 3
}
]
@@ -0,0 +1,380 @@
[
{
"requestedHan": 25000,
"hanCharacters": 25632,
"sourceCharacters": 35728,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 398,
"domNodes": 2110,
"selectionMs": {
"median": 1.0999999940395355,
"p95": 1.9000000059604645,
"max": 1.9000000059604645
},
"insertMs": {
"median": 5.300000011920929,
"p95": 8.100000023841858,
"max": 8.800000011920929
},
"foldMs": {
"median": 58.70000001788139,
"p95": 70.19999998807907,
"max": 70.19999998807907
},
"integrity": true,
"previewMs": [
230.7999999821186,
45.5,
39.70000001788139
],
"previewNodes": 1058,
"longTasks": {
"count": 5,
"median": 84,
"p95": 372,
"max": 372
},
"heapUsedBytes": 42428845,
"repeat": 1
},
{
"requestedHan": 25000,
"hanCharacters": 25632,
"sourceCharacters": 35728,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 272.09999999403954,
"domNodes": 2110,
"selectionMs": {
"median": 1,
"p95": 2.0999999940395355,
"max": 2.399999976158142
},
"insertMs": {
"median": 5.699999988079071,
"p95": 7,
"max": 7.300000011920929
},
"foldMs": {
"median": 48.900000005960464,
"p95": 55,
"max": 55
},
"integrity": true,
"previewMs": [
157,
37.69999998807907,
40
],
"previewNodes": 1058,
"longTasks": {
"count": 6,
"median": 63,
"p95": 245,
"max": 245
},
"heapUsedBytes": 62409024,
"repeat": 2
},
{
"requestedHan": 25000,
"hanCharacters": 25632,
"sourceCharacters": 35728,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 224.90000000596046,
"domNodes": 2110,
"selectionMs": {
"median": 0.7000000178813934,
"p95": 1.4000000059604645,
"max": 2
},
"insertMs": {
"median": 4.399999976158142,
"p95": 6.4000000059604645,
"max": 8
},
"foldMs": {
"median": 39.099999994039536,
"p95": 44.10000002384186,
"max": 44.10000002384186
},
"integrity": true,
"previewMs": [
139,
46.099999994039536,
39.900000005960464
],
"previewNodes": 1058,
"longTasks": {
"count": 4,
"median": 92,
"p95": 200,
"max": 200
},
"heapUsedBytes": 35903770,
"repeat": 3
},
{
"requestedHan": 60000,
"hanCharacters": 61172,
"sourceCharacters": 85716,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 458.39999997615814,
"domNodes": 4720,
"selectionMs": {
"median": 2.9000000059604645,
"p95": 5.699999988079071,
"max": 6.0999999940395355
},
"insertMs": {
"median": 7.5999999940395355,
"p95": 9.699999988079071,
"max": 12.400000005960464
},
"foldMs": {
"median": 120.30000001192093,
"p95": 160.09999999403954,
"max": 160.09999999403954
},
"integrity": true,
"previewMs": [
173.5,
65.60000002384186,
70.09999999403954
],
"previewNodes": 2560,
"longTasks": {
"count": 12,
"median": 110,
"p95": 415,
"max": 415
},
"heapUsedBytes": 60644006,
"repeat": 1
},
{
"requestedHan": 60000,
"hanCharacters": 61172,
"sourceCharacters": 85716,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 649.6999999880791,
"domNodes": 4720,
"selectionMs": {
"median": 2.699999988079071,
"p95": 5.699999988079071,
"max": 5.800000011920929
},
"insertMs": {
"median": 7.5,
"p95": 9.099999994039536,
"max": 10.699999988079071
},
"foldMs": {
"median": 116,
"p95": 149.09999999403954,
"max": 149.09999999403954
},
"integrity": true,
"previewMs": [
209.30000001192093,
80.90000000596046,
88.7999999821186
],
"previewNodes": 2560,
"longTasks": {
"count": 12,
"median": 116,
"p95": 586,
"max": 586
},
"heapUsedBytes": 64379838,
"repeat": 2
},
{
"requestedHan": 60000,
"hanCharacters": 61172,
"sourceCharacters": 85716,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 519.7000000178814,
"domNodes": 4720,
"selectionMs": {
"median": 2.9000000059604645,
"p95": 4.9000000059604645,
"max": 5.5
},
"insertMs": {
"median": 9.200000017881393,
"p95": 11.700000017881393,
"max": 11.900000005960464
},
"foldMs": {
"median": 118.40000000596046,
"p95": 134.5,
"max": 134.5
},
"integrity": true,
"previewMs": [
198.5,
84.80000001192093,
91.59999999403954
],
"previewNodes": 2560,
"longTasks": {
"count": 11,
"median": 119,
"p95": 469,
"max": 469
},
"heapUsedBytes": 43218088,
"repeat": 3
},
{
"requestedHan": 120000,
"hanCharacters": 122322,
"sourceCharacters": 171613,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 894.6000000238419,
"domNodes": 9139,
"selectionMs": {
"median": 7.9000000059604645,
"p95": 11.599999994039536,
"max": 24.700000017881393
},
"insertMs": {
"median": 18.19999998807907,
"p95": 22.19999998807907,
"max": 30.799999982118607
},
"foldMs": {
"median": 247.59999999403954,
"p95": 256.59999999403954,
"max": 256.59999999403954
},
"integrity": true,
"previewMs": [
294.2999999821186,
134.09999999403954,
129.90000000596046
],
"previewNodes": 5117,
"longTasks": {
"count": 14,
"median": 207,
"p95": 821,
"max": 821
},
"heapUsedBytes": 60930415,
"repeat": 1
},
{
"requestedHan": 120000,
"hanCharacters": 122322,
"sourceCharacters": 171613,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 875.3999999761581,
"domNodes": 9139,
"selectionMs": {
"median": 9.199999988079071,
"p95": 15.199999988079071,
"max": 15.900000005960464
},
"insertMs": {
"median": 20.599999994039536,
"p95": 23.19999998807907,
"max": 26.5
},
"foldMs": {
"median": 244.09999999403954,
"p95": 269,
"max": 269
},
"integrity": true,
"previewMs": [
272.2999999821186,
146.30000001192093,
157.40000000596046
],
"previewNodes": 5117,
"longTasks": {
"count": 14,
"median": 192,
"p95": 785,
"max": 785
},
"heapUsedBytes": 31982024,
"repeat": 2
},
{
"requestedHan": 120000,
"hanCharacters": 122322,
"sourceCharacters": 171613,
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36",
"viewport": [
1424,
905
],
"openMs": 842.3000000119209,
"domNodes": 9139,
"selectionMs": {
"median": 8,
"p95": 11.900000005960464,
"max": 15.199999988079071
},
"insertMs": {
"median": 18.900000005960464,
"p95": 20.099999994039536,
"max": 21.700000017881393
},
"foldMs": {
"median": 212.5,
"p95": 240.5,
"max": 240.5
},
"integrity": true,
"previewMs": [
253.7000000178814,
129.19999998807907,
172
],
"previewNodes": 5117,
"longTasks": {
"count": 14,
"median": 191,
"p95": 762,
"max": 762
},
"heapUsedBytes": 49460811,
"repeat": 3
}
]
@@ -0,0 +1,122 @@
[
{
"requestedHan": 25000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 10,
"p95": 20.100000000000364,
"max": 110.09999999999991
},
"frames": 380,
"framesOver25ms": 14,
"longTasks": [
97
],
"scrollTop": 6111,
"scrollHeight": 33908,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 25000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 10,
"p95": 20.100000000000136,
"max": 50
},
"frames": 374,
"framesOver25ms": 15,
"longTasks": [
57
],
"scrollTop": 6111,
"scrollHeight": 33908,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
},
{
"requestedHan": 60000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 30.100000000000364,
"p95": 50,
"max": 100
},
"frames": 267,
"framesOver25ms": 210,
"longTasks": [
81,
84
],
"scrollTop": 52492,
"scrollHeight": 80287,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 60000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 30,
"p95": 40.100000000000364,
"max": 100
},
"frames": 279,
"framesOver25ms": 213,
"longTasks": [
77,
59
],
"scrollTop": 52493,
"scrollHeight": 80287,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 70,
"p95": 100,
"max": 130
},
"frames": 264,
"framesOver25ms": 229,
"longTasks": [
84,
52,
89,
58
],
"scrollTop": 53157,
"scrollHeight": 159554,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 70.09999999999854,
"p95": 100,
"max": 130.10000000000036
},
"frames": 261,
"framesOver25ms": 230,
"longTasks": [
83,
53
],
"scrollTop": 53195,
"scrollHeight": 159554,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
}
]
@@ -0,0 +1,119 @@
[
{
"requestedHan": 25000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 10,
"p95": 30.09999999999991,
"max": 120.10000000000002
},
"frames": 356,
"framesOver25ms": 45,
"longTasks": [
116
],
"scrollTop": 6075,
"scrollHeight": 33871,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 25000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 10,
"p95": 30,
"max": 80.10000000000002
},
"frames": 369,
"framesOver25ms": 29,
"longTasks": [
93
],
"scrollTop": 6111,
"scrollHeight": 33908,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
},
{
"requestedHan": 60000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 40,
"p95": 59.900000000001455,
"max": 100.09999999999991
},
"frames": 264,
"framesOver25ms": 221,
"longTasks": [
92
],
"scrollTop": 52345,
"scrollHeight": 80177,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 60000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 40,
"p95": 50.100000000000364,
"max": 130.10000000000036
},
"frames": 265,
"framesOver25ms": 229,
"longTasks": [
84,
109
],
"scrollTop": 52344,
"scrollHeight": 80177,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 70,
"p95": 90.10000000000036,
"max": 170.19999999999982
},
"frames": 260,
"framesOver25ms": 235,
"longTasks": [
89,
77
],
"scrollTop": 53200,
"scrollHeight": 159554,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 1
},
{
"requestedHan": 120000,
"theme": "paper-moments",
"frameGapsMs": {
"median": 70,
"p95": 90.10000000000036,
"max": 140.10000000000036
},
"frames": 259,
"framesOver25ms": 228,
"longTasks": [
92,
57
],
"scrollTop": 53196,
"scrollHeight": 159554,
"foldedScrollTop": 0,
"caretAfterFold": 1,
"repeat": 2
}
]
@@ -0,0 +1,79 @@
# 长文渲染优化与压测报告
日期:2026-09-06。基线为 `750e172`,结果为本次未提交的渲染优化;基线和优化版使用相同样本、浏览器参数及操作序列。
## 1. 测试环境与范围
Windows、Intel Core i7-13620H、Headless Chrome 152,视口 1424 × 905,Vite 开发服务。每种规模重开测试页面三次,独立测试浏览器,不读写真实笔记。耗时基于 performance.now;打开指标不包含模块网络加载。样本含标题、表格、代码块、提示框和行内格式。
实际汉字数为 25632、61172、122322Markdown 总字符数以原始 JSON 为准。不是仅以文件字节数代替“字数”。共完成优化前后 18 轮浏览器运行,所有运行的插入内容与文末完整性检查通过。
## 2. 本次优化
- 标题装饰按不可变文档和折叠状态弱缓存;光标移动不改变折叠状态时复用结果。没有折叠范围时不遍历全文生成隐藏装饰。
- 文档变更后用位置 Set 验证标题映射,避免对每个已折叠标题反复扫描所有标题。
- 字号装饰按不可变文档弱缓存;提示框标记位置按文档缓存,光标移动只判断标记是否处于编辑范围。
- 静态代码高亮增加 LRU,最多 64 项、累计 100 万个字符串字符;超过预算四分之一的单块不缓存。缓存同时包含深浅主题颜色,最终输出继续净化。缓存只在进程内,不写入存储。
弱缓存随文档释放,不用全局 Map 持有历史文档;高亮缓存有明确上限。
## 3. 对比结果
单位 ms。打开取三轮中位数;选区、插入、折叠取各轮 P95 的中位数;热预览取六次热运行中位数。P95 样本量较少,仅用于本机前后比较。
| 实际汉字数 | 打开 前→后 | 光标移动 P95 前→后 | 插入 P95 前→后 | 折叠/展开 P95 前→后 | 热预览 前→后 |
| --- | --- | --- | --- | --- | --- |
| 25632 | 272.1 → 218.6 | 1.9 → 0.6 | 7.0 → 6.9 | 55.0 → 45.6 | 40.0 → 26.6 |
| 61172 | 519.7 → 447.5 | 5.7 → 0.6 | 9.7 → 9.6 | 149.1 → 145.5 | 82.9 → 55.6 |
| 122322 | 875.4 → 869.1 | 11.9 → 1.2 | 22.2 → 27.2 | 256.6 → 263.9 | 140.2 → 95.5 |
主要收益是减少光标移动的重复工作,以及重复代码块/重复预览的高亮开销。2.5 万汉字样本完成全部操作,但不以此宣称所有文档都能维持 60 FPS。
12 万汉字的整篇折叠仍超过 250 ms,插入 P95 本轮反而增加,不能声称打字性能全面提升;全量 DOM 更新、编辑器自身处理与浏览器调度仍需进一步分项剖析。打开仍有长任务,缓存没有解决虚拟化问题。
## 4. 原始数据与复现
- [优化前 JSON](performance/2026-09-06-long-document-baseline.json)
- [优化后 JSON](performance/2026-09-06-long-document-after.json)
- [压测脚本与指标说明](../../frontend/tests/performance/README.md)
```powershell
npm --prefix frontend run test
npm --prefix frontend run build
npm --prefix frontend run dev -- --port 5175 --strictPort
# 另一个终端:
backend/.venv/Scripts/python.exe frontend/tests/performance/run-stress.py --url http://127.0.0.1:5175/tests/performance/stress.html --runs 3 --output .local-plans/stress-results.json
```
新增回归检查确认光标移动复用标题装饰、正文编辑使缓存失效、折叠与展开不残留隐藏内容。前端 402 项测试通过。
## 5. 边界与后续
本次测量是浏览器中的程序化编辑事务,不是输入法、实际键盘到屏幕显示、滚动帧率或生产包启动测试。样本代码块存在重复,LRU 收益不能直接外推到全部唯一代码块的文档。堆内存只采样,不作为内存泄漏结论。
后续应针对 12 万字的输入和整篇折叠采集独立 CPU trace,评估装饰的增量映射及可见区域渲染;在输入法、表格与代码块编辑验收之前,不直接给 contenteditable 应用跳过布局或整篇虚拟化。
## 6. 手动压测反馈后的滚动与定位修复
用户实测:2.56 万字正常,6 万字快速滚动轻微停顿,12 万字明显卡顿;从长文末尾全部折叠没有回到顶层。
本轮将“全部折叠”设为一次性把光标移到首个标题并将编辑区滚动到顶部,单个标题折叠保留原有定位行为。回归用例包含 100 个同级顶层标题,从文末触发后检查光标、scrollTop 与正文不变,避免只有一个 H1 的样本掩盖问题。
语言标签改为只同步变动的代码块,语言菜单只在打开时响应滚动定位,并合并同一帧的重复事件;监听祖先滚动,卸载时清理监听及待执行帧。
新增独立 CDP 滚轮测试,在相同的有限高度 flex 容器、纸间时光主题下,每档运行两次。下表为每次帧间隔 P95 的中位数,单位 ms,越小越好。这不是原先编辑事务基准的延续,也不是生产环境 FPS。
| 请求汉字数 | 修改前 | 修改后 |
| --- | ---: | ---: |
| 25000 | 30.05 | 20.10 |
| 60000 | 55.00 | 45.05 |
| 120000 | 90.10 | 100.00 |
两档较短样本有改善,12 万字没有改善,仍未达到流畅滚动的目标。CPU 采样显示滚入新代码块时的 CodeMirror 初始化、选区测量及 DOM 观察回调占用明显;不能仅凭此认定唯一原因。代码块布局隔离实验未带来收益,已撤回,未引入可能影响编辑、选区或布局的虚拟化。
- [滚轮基线数据](performance/2026-09-06-scroll-baseline.json)
- [滚轮修改后数据](performance/2026-09-06-scroll-after.json)
下一步需要评估代码块按交互初始化或编辑器级虚拟化,并同时验证输入、选区、搜索定位和折叠;当前不将 12 万字滚动问题标记为完成。
本轮前端 405 项测试通过,涵盖折叠定位、语言标签增量同步、菜单祖先滚动定位和卸载清理。