-
Notifications
You must be signed in to change notification settings - Fork 296
/
Copy pathatom.xml
516 lines (291 loc) · 325 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Here. There.</title>
<subtitle>Love ice cream. Love sunshine. Love life. Love the world. Love myself. Love you.</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="https://godbasin.github.io/"/>
<updated>2025-01-02T13:34:16.651Z</updated>
<id>https://godbasin.github.io/</id>
<author>
<name>被删</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>前端性能优化--二进制压缩数据内容</title>
<link href="https://godbasin.github.io/2025/01/02/front-end-performance-binary-attribute/"/>
<id>https://godbasin.github.io/2025/01/02/front-end-performance-binary-attribute/</id>
<published>2025-01-02T13:34:23.000Z</published>
<updated>2025-01-02T13:34:16.651Z</updated>
<content type="html"><![CDATA[<p>今天也是来介绍一种性能优化的具体方式,使用二进制存储特定数据,来降低内存占用、后台存储和传输成本。</p><h2 id="二进制数据设计"><a href="#二进制数据设计" class="headerlink" title="二进制数据设计"></a>二进制数据设计</h2><p>当我们需要描述某种数据的许多状态时,可以考虑使用二进制的方式优化。</p><p>简单来说,就是使用二进制数字<code>1</code>和<code>0</code>来表示单个状态,然后使用二进制数字来表示多种状态的组合,比如<code>10001001</code>可以表示 8 种状态。同时还可以将二进制转换为十进制来减少存储成本,比如<code>10001001</code>可转换成<code>137</code>。</p><h3 id="上报数据转换"><a href="#上报数据转换" class="headerlink" title="上报数据转换"></a>上报数据转换</h3><p>常用的场景可以考虑数据上报,比如当用户在编辑文档/打开文档的时候,需要收集一些数据来观测性能情况比如:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">IDocumentInfo</span> {</span><br><span class="line"> <span class="attr">isHugeDocument</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isReadonly</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasCharts</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasFormatting</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasImages</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasVideos</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasRadios</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasPivotTable</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasTableStyle</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">hasFreezePanels</span>: <span class="built_in">boolean</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们需要根据文档的具体情况,结合大盘的文档性能数据来判断加载速度是否与某些文档特性相关,假设一次上报数据为:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> reportData {</span><br><span class="line"> <span class="attr">timecost</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="attr">docInfo</span>: <span class="title class_">IDocumentInfo</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当希望收集的数据多了之后,我们每次都会携带十分大的数据内容。这时候可以考虑使用二进制的方式来进行上报,比如:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> docInfo = {</span><br><span class="line"> <span class="attr">isHugeDocument</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">isReadonly</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasCharts</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasFormatting</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasImages</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasVideos</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasRadios</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasPivotTable</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">hasTableStyle</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">hasFreezePanels</span>: <span class="literal">true</span>,</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>可以表示为 10 个二进制位,即:<code>1110100011</code>,那么转为十进制则是<code>931</code>,我们上报为<code>931</code>即可。</p><p>通过这样的方式,原本一个 JSON 字符串:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">"{\"isHugeDocument\":true,\"isReadonly\":true,\"hasCharts\":true,\"hasFormatting\":false,\"hasImages\":true,\"hasVideos\":false,\"hasRadios\":false,\"hasPivotTable\":false,\"hasTableStyle\":true,\"hasFreezePanels\":true}"</span></span><br></pre></td></tr></table></figure><p>只需要使用<code>931</code>来表示,可以极大地节省传输和存储成本。</p><h3 id="单元格数据状态"><a href="#单元格数据状态" class="headerlink" title="单元格数据状态"></a>单元格数据状态</h3><p>除了上报数据以外,多状态数据同样适应。使用表格为例,一个单元格的样式可能包括:加粗、下划线、删除线、斜体、字号、字体颜色、字体样式、背景色等。那么,我们需要这样来描述一个单元格数据:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">ICell</span> {</span><br><span class="line"> <span class="attr">isBold</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isItalic</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isUnderline</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">isStrikeThrough</span>: <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">font</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">fontSize</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果我们对一个单元格进行样式编辑,假设只设置了加粗、下划线,按理说最简单的数据变更应该只有<code>isBold</code>和<code>isUnderline</code>两个新数据,那么这次变更我们可以认为是<code>10100000</code>,转换成十进制则是<code>160</code>。</p><p>如果说我们还需要再细致些,直接将单元格的最终状态进行存储,同样可以使用进制的方式进行,比如<code>boolean</code>类型的可以直接使用二进制表示最终状态,假设前面四个布尔值的加粗、下划线、删除线、斜体可以压缩为<code>1010</code>来表示一个加粗、无下划线、有删除线、无斜体样式的单元格。</p><p>那么,我们在存储单元格数据的时候,一些小的成本节约遇上百万千万单元格数据时,则可能会产生想象不到的优化效果。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>今天介绍的只是一个内存优化的思路,但依然还是一句话,性能优化往往是时间换空间、或是空间换时间,本文的例子中显然是时间换空间,毕竟我们需要对数据进行转换,这个过程需要消耗时间是不可避免的。</p><p>除了二进制转换成十进制以外,这样的思路可以拓展到许多地方,比如 16 进制/ 32 进制,甚至简单的字符串拼接等。本质上都是使用约定的方式来存储数据内容,比如 pb、json 便都是一种约定的数据结构。</p><p>还是那句,没有适用于所有方案的最优解,但总有更适合某个场景的解决方案。</p>]]></content>
<summary type="html">
<p>今天也是来介绍一种性能优化的具体方式,使用二进制存储特定数据,来降低内存占用、后台存储和传输成本。</p>
<h2 id="二进制数据设计"><a href="#二进制数据设计" class="headerlink" title="二进制数据设计"></a>二进制数据设计<
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--代码习惯</title>
<link href="https://godbasin.github.io/2024/12/27/front-end-performance-code-detail/"/>
<id>https://godbasin.github.io/2024/12/27/front-end-performance-code-detail/</id>
<published>2024-12-27T15:25:12.000Z</published>
<updated>2024-12-27T15:25:46.536Z</updated>
<content type="html"><![CDATA[<p>大多数情况下,前端很少遇到性能瓶颈。但如果在大型前端项目、数据量百万千万的场景下,有时候一些毫不起眼的代码习惯也可能会带来性能问题。</p><p>今天来简单介绍几种,大家在写代码的时候也可以注意。</p><h2 id="代码细节与性能"><a href="#代码细节与性能" class="headerlink" title="代码细节与性能"></a>代码细节与性能</h2><h3 id="减少函数拆解"><a href="#减少函数拆解" class="headerlink" title="减少函数拆解"></a>减少函数拆解</h3><p>很多时候,为了提高代码复用率以及提升代码可读性,我们习惯地将一些相同逻辑的代码进行抽离,比如下述的代码:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 检查两个范围是否有相交</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">checkTwoDimensionCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> startA: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> endA: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> startB: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> endB: <span class="built_in">number</span></span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> !(startA > endB || endA < startB);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 检查两个列范围是否有相交</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">checkTwoColRangesCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> colRangeA: [<span class="built_in">number</span>, <span class="built_in">number</span>],</span></span><br><span class="line"><span class="params"> colRangeB: [<span class="built_in">number</span>, <span class="built_in">number</span>]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">const</span> [startColA, endColA] = colRangeA;</span><br><span class="line"> <span class="keyword">const</span> [startColB, endColB] = colRangeB;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">checkTwoDimensionCross</span>(startColA, endColA, startColB, endColB);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 检查两个行范围是否有相交</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">checkTwoRowRangeCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> areaA: { rowStart: <span class="built_in">number</span>; rowEnd: <span class="built_in">number</span> },</span></span><br><span class="line"><span class="params"> areaB: { rowStart: <span class="built_in">number</span>; rowEnd: <span class="built_in">number</span> }</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">checkTwoDimensionCross</span>(</span><br><span class="line"> areaA.<span class="property">rowStart</span>,</span><br><span class="line"> areaA.<span class="property">rowEnd</span>,</span><br><span class="line"> areaB.<span class="property">rowStart</span>,</span><br><span class="line"> areaB.<span class="property">rowEnd</span></span><br><span class="line"> );</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在该代码中,由于行范围和列范围的类型不一致,但为了逻辑判断一致性和方便管理,我们抽离了<code>checkTwoDimensionCross</code>方法,用于判断两个一维的范围是否相交。</p><p>大多数情况下,考虑代码可读性,也比较推荐这种写法。但如果在十万百万次调用的函数方法里,多一层的函数就需要多一层调用栈的开销,其中性能的影响不可小觑。因此,我们可以将拆出去的函数合并回来:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 检查两个行范围是否有相交</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">checkTwoRowRangeCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> areaA: IRowRange,</span></span><br><span class="line"><span class="params"> areaB: IRowRange</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> !(areaA.<span class="property">rowStart</span> > areaB.<span class="property">rowEnd</span> || areaA.<span class="property">rowEnd</span> < areaB.<span class="property">rowStart</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 检查两个列范围是否有相交</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">checkTwoColRangesCross</span>(<span class="params"></span></span><br><span class="line"><span class="params"> colRangeA: IColRange,</span></span><br><span class="line"><span class="params"> colRangeB: IColRange</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">const</span> [startColA, endColA] = colRangeA;</span><br><span class="line"> <span class="keyword">const</span> [startColB, endColB] = colRangeB;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> !(startColA > endColB || endColA < startColB);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="if-else-或许性能更优"><a href="#if-else-或许性能更优" class="headerlink" title="if else 或许性能更优"></a>if else 或许性能更优</h3><p>有时候我们为了偷懒,喜欢使用语法糖来缩减代码的编写,比如说判断两个字符串数组是否内容一致:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 判断两个字符串数组是否内容一致</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isStringArrayTheSame</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> stringArrayA.<span class="title function_">sort</span>().<span class="title function_">join</span>(<span class="string">","</span>) === stringArrayB.<span class="title function_">sort</span>().<span class="title function_">join</span>(<span class="string">","</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但同样的,假设这个方法被调用十万百万次,性能问题可能就会变得是否明显,不管是<code>sort</code>还是数组拼接成字符串都会有一定开销。这种情况下我们可以这么写:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 使用场景为数组内的字符串不会重复</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">isStringArrayTheSame</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="comment">// 数量不一致,肯定不同</span></span><br><span class="line"> <span class="keyword">if</span> (stringArrayA.<span class="property">length</span> !== stringArrayB.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 相同数量时,A 的每一个都应该存在 B 中,才完全一致</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> <span class="keyword">type</span> <span class="keyword">of</span> stringArrayA) {</span><br><span class="line"> <span class="keyword">if</span> (!stringArrayB.<span class="title function_">includes</span>(<span class="keyword">type</span>)) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面这种偷懒写法也是:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// bad</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">mergeStringArray</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">string</span>[] {</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Array</span>.<span class="title function_">from</span>(<span class="keyword">new</span> <span class="title class_">Set</span>(stringArrayA.<span class="title function_">concat</span>(stringArrayB)));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// good</span></span><br><span class="line"><span class="comment">// 使用场景为单数组内的字符串不会重复</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">mergeStringArray</span>(<span class="params"></span></span><br><span class="line"><span class="params"> stringArrayA: <span class="built_in">string</span>[],</span></span><br><span class="line"><span class="params"> stringArrayB: <span class="built_in">string</span>[]</span></span><br><span class="line"><span class="params"></span>): <span class="built_in">string</span>[] {</span><br><span class="line"> <span class="keyword">const</span> newStringArray = [].<span class="title function_">concat</span>(stringArrayA);</span><br><span class="line"> stringArrayB.<span class="title function_">forEach</span>(<span class="function">(<span class="params"><span class="keyword">type</span></span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!newStringArray.<span class="title function_">includes</span>(<span class="keyword">type</span>)) newStringArray.<span class="title function_">push</span>(<span class="keyword">type</span>);</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> newStringArray;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="低性能消耗代码判断提前"><a href="#低性能消耗代码判断提前" class="headerlink" title="低性能消耗代码判断提前"></a>低性能消耗代码判断提前</h3><p><code>if...else</code>写法也有很多注意事项,最简单的莫过于尽量使执行代码提前<code>return</code>。假设我们现在有这样的代码:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params">arrayA: <span class="built_in">string</span>[], arrayB: <span class="built_in">string</span>[]</span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="title function_">costTimeFunction</span>(arrayA, arrayB) || <span class="title function_">noCostTimeFunction</span>(arrayA, arrayB)) {</span><br><span class="line"> <span class="title function_">testCodeA</span>();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="title function_">testCodeB</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样写看起来没什么问题,但假设已知<code>costTimeFunction</code>函数执行会有一定的性能消耗,那么在数组长度很大、调用次数很多的情况下,我们可以将耗时较少的函数放在前面执行:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params">arrayA: <span class="built_in">string</span>[], arrayB: <span class="built_in">string</span>[]</span>): <span class="built_in">boolean</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="title function_">noCostTimeFunction</span>(arrayA, arrayB) || <span class="title function_">costTimeFunction</span>(arrayA, arrayB)) {</span><br><span class="line"> <span class="title function_">testCodeA</span>();</span><br><span class="line"> <span class="comment">// 提前 retrun 可以简化代码复杂度</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="title function_">testCodeB</span>();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>虽然这些都是很细节的事情,有时候写代码甚至注意不到,但如果养成了思考代码性能的习惯,就可以写出更高效执行的代码。</p><p>实际上,除了简单的代码习惯以外,更多时候我们的性能问题也往往出现在不合理的代码执行流程里,这种就跟项目关系紧密,不在这里介绍啦。</p>]]></content>
<summary type="html">
<p>大多数情况下,前端很少遇到性能瓶颈。但如果在大型前端项目、数据量百万千万的场景下,有时候一些毫不起眼的代码习惯也可能会带来性能问题。</p>
<p>今天来简单介绍几种,大家在写代码的时候也可以注意。</p>
<h2 id="代码细节与性能"><a href="#代码细节与性
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--享元模式</title>
<link href="https://godbasin.github.io/2024/11/06/front-end-performance-flyweight-pattern/"/>
<id>https://godbasin.github.io/2024/11/06/front-end-performance-flyweight-pattern/</id>
<published>2024-11-06T12:51:10.000Z</published>
<updated>2024-11-06T12:51:54.122Z</updated>
<content type="html"><![CDATA[<p>之前讲到性能优化,大多数介绍的都是耗时上的一些优化,比如页面打开更快、用户交互响应更快等。不过,在最开始的<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>一文中有说过,前端性能优化可以从两个角度来衡量:时间和空间,今天介绍的享元模式则用于空间下内存占用的优化。</p><h2 id="享元模式"><a href="#享元模式" class="headerlink" title="享元模式"></a>享元模式</h2><p>享元是一种设计模式,通过共享对象的方式来减少创建对象的数量,从而降低程序运行过程中占用的内存,提升页面性能。</p><p>一般来说,假如我们的页面中存在大量相类似的内容时,这些内容在代码中被设计为对象的方式,则我们可以通过享元的方式,将一样的对象进行共享,从而减少页面中的总对象数,降低内存占用。</p><p>本文就以最近比较熟练的表格为例子来介绍吧。</p><h3 id="享元对象设计"><a href="#享元对象设计" class="headerlink" title="享元对象设计"></a>享元对象设计</h3><p>假设我们现在有 1W 个单元格的表格,每个单元格内都有不一样的文字信息,但是单元格格式基本上都是一样的,这里包括字体色、背景色、对齐方式等等格式。</p><p>我们可以将这样一个格式<code>CellStyle</code>作为享元对象,它的属性可能包括:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="keyword">enum</span> <span class="title class_">HorizontalAlign</span> {</span><br><span class="line"> left = <span class="string">"left"</span>,</span><br><span class="line"> center = <span class="string">"center"</span>,</span><br><span class="line"> right = <span class="string">"right"</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">enum</span> <span class="title class_">VerticalAlign</span> {</span><br><span class="line"> top = <span class="string">"top"</span>,</span><br><span class="line"> middle = <span class="string">"middle"</span>,</span><br><span class="line"> bottom = <span class="string">"bottom"</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ICellStyleProps</span> {</span><br><span class="line"> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>;</span><br><span class="line"> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么一个<code>CellStyle</code>对象则可能是这样的:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CellStyle</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">constructor</span>(<span class="params">{</span></span><br><span class="line"><span class="params"> textColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> backgroundColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> horizontalAlign: HorizontalAlign,</span></span><br><span class="line"><span class="params"> verticalAlign: VerticalAlign,</span></span><br><span class="line"><span class="params"> }</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">textColor</span> = textColor || <span class="string">"#000"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">backgroundColor</span> = backgroundColor || <span class="string">"#fff"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span> = horizontalAlign || <span class="title class_">HorizontalAlign</span>.<span class="property">left</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">verticalAlign</span> = verticalAlign || <span class="title class_">VerticalAlign</span>.<span class="property">middle</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">textColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">textColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">backgroundColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">backgroundColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">horizontalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">verticalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">verticalAlign</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>一个单元格可能是这样的:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Cell</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">row</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">column</span>: <span class="built_in">number</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">cellStyle</span>: <span class="title class_">CellStyle</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">text</span>: <span class="built_in">string</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> row: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> column: <span class="built_in">number</span>,</span></span><br><span class="line"><span class="params"> text: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> cellStyle?: CellStyle</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">row</span> = row;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">column</span> = column;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">text</span> = text;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">cellStyle</span> = cellStyle || <span class="keyword">new</span> <span class="title class_">CellSyle</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由于单元格跟行列信息<code>row/column</code>挂钩,因此是无法完全享元的,那么 1W 个单元格的表格里可能有 1W 个<code>Cell</code>对象。同样的,每个<code>Cell</code>对象都有一个<code>CellStyle</code>对象,因此该表格同样会有 1W 个<code>CellStyle</code>对象。</p><p>但是<code>CellStyle</code>对象仅跟单元格的格式相关,我们可以考虑将<code>CellStyle</code>对象进行享元。</p><h3 id="享元工厂"><a href="#享元工厂" class="headerlink" title="享元工厂"></a>享元工厂</h3><p>我们可以给<code>CellStyle</code>定义一个享元的<code>key</code>,当然这个<code>key</code>可以代表完全相同格式的<code>CellStyle</code>对象,并通过享元的方式创建对象:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CellStyle</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="attr">pools</span>: {</span><br><span class="line"> [<span class="attr">key</span>: <span class="built_in">string</span>]: <span class="title class_">CellStyle</span>;</span><br><span class="line"> } = {};</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="title function_">generateKey</span>(<span class="params"></span></span><br><span class="line"><span class="params"> textColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> backgroundColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> horizontalAlign: HorizontalAlign,</span></span><br><span class="line"><span class="params"> verticalAlign: VerticalAlign</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<span class="subst">${textColor}</span>-<span class="subst">${backgroundColor}</span>-<span class="subst">${horizontalAlign}</span>-<span class="subst">${verticalAlign}</span>`</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">static</span> <span class="title function_">newInstance</span>(<span class="attr">props</span>: <span class="title class_">ICellStyleProps</span>): <span class="title class_">CellStyle</span> {</span><br><span class="line"> <span class="keyword">const</span> { textColor, backgroundColor, horizontalAlign, verticalAlign } =</span><br><span class="line"> props;</span><br><span class="line"> <span class="keyword">const</span> key = <span class="title class_">CellStyle</span>.<span class="title function_">generateKey</span>(</span><br><span class="line"> textColor,</span><br><span class="line"> backgroundColor,</span><br><span class="line"> horizontalAlign,</span><br><span class="line"> verticalAlign</span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果已有相同格式的对象,则使用享元对象</span></span><br><span class="line"> <span class="keyword">const</span> cellStyle = <span class="title class_">CellStyle</span>.<span class="property">pools</span>[key];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果没有,则创建享元对象,并添加到享元对象池子</span></span><br><span class="line"> <span class="keyword">return</span> cellStyle</span><br><span class="line"> ? cellStyle</span><br><span class="line"> : (<span class="title class_">CellStyle</span>.<span class="property">pools</span>[key] = <span class="keyword">new</span> <span class="title class_">CellStyle</span>(</span><br><span class="line"> textColor,</span><br><span class="line"> backgroundColor,</span><br><span class="line"> horizontalAlign,</span><br><span class="line"> verticalAlign</span><br><span class="line"> ));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">textColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">backgroundColor</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> textColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> backgroundColor: <span class="built_in">string</span>,</span></span><br><span class="line"><span class="params"> horizontalAlign: HorizontalAlign,</span></span><br><span class="line"><span class="params"> verticalAlign: VerticalAlign</span></span><br><span class="line"><span class="params"> </span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">textColor</span> = textColor || <span class="string">"#000"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">backgroundColor</span> = backgroundColor || <span class="string">"#fff"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span> = horizontalAlign || <span class="title class_">HorizontalAlign</span>.<span class="property">left</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">verticalAlign</span> = verticalAlign || <span class="title class_">VerticalAlign</span>.<span class="property">middle</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">textColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">textColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">backgroundColor</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">backgroundColor</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">horizontalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">horizontalAlign</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">get</span> <span class="title function_">verticalAlign</span>() {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">verticalAlign</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>相比于<code>new CellStyle()</code>的方式创建对象,我们可以使用<code>CellStyle.newInstance()</code>的方式来创建:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> cellStyle = <span class="title class_">CellStyle</span>.<span class="title function_">newInstance</span>({</span><br><span class="line"> <span class="attr">textColor</span>: <span class="string">"#000"</span>,</span><br><span class="line"> <span class="attr">backgroundColor</span>: <span class="string">"#fff"</span>,</span><br><span class="line"> <span class="attr">horizontalAlign</span>: <span class="title class_">HorizontalAlign</span>.<span class="property">center</span>,</span><br><span class="line"> <span class="attr">verticalAlign</span>: <span class="title class_">VerticalAlign</span>.<span class="property">top</span>,</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>到这里,如果我们表格中 1W 个单元格的样式都是一样的,那么我们页面中只会存在一个<code>CellStyle</code>对象,大幅度减少了对象的创建和维护,降低了页面的内存占用,从而提升页面的性能。</p><p>当然,享元并不是万能的。前端性能优化的尽头往往是时间换空间、空间换时间,享元便是一个时间换空间的典型例子,我们通过<code>key</code>去获取对象时会比直接访问要多一步。</p><p>除此之外,享元对象需要十分注意对象的修改。由于对象是享元的,如果在使用的时候直接修改了,会导致许多引用到的地方都被修改。因此,一般建议通过<code>CellStyle.newInstance()</code>新建<code>CellStyle</code>对象的方式来进行修改。</p><p>最后其实还留了个小问题给小伙伴们想一想,<code>CellStyle</code>中颜色是字符串的形式提供的,但前端颜色表示可能不只有一个,比如<code>#000</code>、<code>#000000</code>和<code>rgb(0,0,0)</code>都是代表一种颜色,那么在<code>key</code>中如何让它们保持一致呢?</p><h3 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h3><p>之前将性能优化都倾向于介绍比较大的解决方案,后面如果有时间的话,也会考虑一个个小的优化点拎出来简单讲讲,比如享元就是其中一个。</p><p>有时候一点小小的问题里,也会有许多学问可以学习的!</p>]]></content>
<summary type="html">
<p>之前讲到性能优化,大多数介绍的都是耗时上的一些优化,比如页面打开更快、用户交互响应更快等。不过,在最开始的<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>项目中的技术债务</title>
<link href="https://godbasin.github.io/2024/10/27/tech-debt/"/>
<id>https://godbasin.github.io/2024/10/27/tech-debt/</id>
<published>2024-10-27T13:12:22.000Z</published>
<updated>2024-10-27T13:13:53.883Z</updated>
<content type="html"><![CDATA[<p>身为一名程序员,我们经常会调侃自己每天的工作就是在屎山上拉屎。这里的屎山还有一个更好的名称,叫做技术债务。</p><h2 id="技术债务是怎么产生的"><a href="#技术债务是怎么产生的" class="headerlink" title="技术债务是怎么产生的"></a>技术债务是怎么产生的</h2><p>我参加过许多不同的项目,而基本上每个项目都会存在或多或少的历史债务。实际上,愿意给到资源去解决历史债务的团队,更是少之又少。</p><p>业务功能的快速迭代,往往意味着缺少长期的规划和设计,架构演化和迭代更是无从谈起。我们总会吐槽自己在屎山上拉屎,但许多项目的实际情况是生命周期很短,业务或许还未稳定就已经被淘汰。</p><p>这样的情况下,技术债务的产生是必然的,我们写的每一行代码都可能成为历史债务。因为我们的业务在不停地快速试错和迭代,项目也在不断地变更和发展。技术方案没有唯一解,最合适的技术方案想必也会跟随着项目产生变化。</p><p>即使我们很幸运地遇到了生命周期较长的项目,也不可避免地在业务快速发展的时候忙于堆叠功能。直到现有架构的维护成本过高,影响到后续功能迭代时,才会想起来需要进行技术变更。</p><p>当架构设计需要进行变更、新技术引入时,过往的方案设计很容易就成为了历史债务,这是一个必然过程。</p><p>虽然技术债务躲不了,那当技术发生变更的时候,我们可以通过一些方法使其产生更少的债务。</p><h2 id="技术方案预研"><a href="#技术方案预研" class="headerlink" title="技术方案预研"></a>技术方案预研</h2><p>这些年的前端技术变更十分迅猛,很多人会在项目中引入新技术,来获得更高的开发效率或是更好的性能。除此之外,我还见过许多技术的引入,单纯是为了跟上新的技术栈,或是拿业务代码来做试验。</p><p>最糟糕的还是为了汇报引入的技术,我在工作中已经见过无数为了晋级答辩强行造的轮子或是引入新技术。在答辩通过之后,他们往往会继续去攻陷下一个“技术亮点”,留下来大堆大堆的技术债务。当然,这也不能怪留下债务的人,很多时候他们也只是想办法在规则范围内获得更多的利益。</p><p>那么,当我们遇到需要引入新的架构设计或是技术的时候,可以进行较深入的技术方案预研,来尽量避免引入更多的技术债务。确保了技术方案的最优化,可以避免开发过程遇到问题需要推翻重做,也可以提前评估预期的效果和引入的技术债务如何解决等问题。</p><p>技术预研的话,可以从几个方面考虑起:</p><ol><li>项目现状/痛点分析。</li><li>业界方案调研/方案选型。</li><li>架构可拓展性。</li></ol><h3 id="项目现状-痛点分析"><a href="#项目现状-痛点分析" class="headerlink" title="项目现状/痛点分析"></a>项目现状/痛点分析</h3><p>在进行技术方案调研的时候,我们需要首先结合自身项目的背景、存在的痛点、现状问题进行分析,只有找到项目的问题在哪里,才可以更准确、彻底地去解决这些问题。</p><p>很多人在拿到一个不熟悉的项目时,第一反应经常是重构它。说实话,要把重构这项工作做好,往往是吃力不讨好。对此,个人的建议是可以先开发和维护一段时间,确切知道项目的实际情况后,结合业务未来的规划,再来考虑是否需要进行重构工作,亦或是局部优化。如果说业务已经稳定,且不会再用什么新的功能了,除非是 bug 多到无法解决,否则就不需要投入过多的精力在这里。</p><p>项目痛点是什么?直白来说,就是我们每天在吐槽的事情,还有我们认为没有意思的事情,比如:糟糕的历史代码、枯燥又重复的开发工作、历史债务导致的开发效率低下等问题。相比于每天都在吐槽,我们可以动动手花点时间把问题解决,这样每天就可以有更多摸鱼的时间了(不是。</p><p>更多情况下,是项目现有的设计,无法支撑后续功能的快速迭代了。比如说,项目代码已经很庞大了,模块之间调用关系过于凌乱、模块状态的数量过多导致修改和监听复杂等等。那么,这种情况下,我们则需要引入新的技术或是架构设计到项目中,比如使用依赖注入来管理模块间的依赖关系,使用状态管理工具来维护应用各模块以及全局的的状态。</p><h3 id="业界方案调研-方案选型"><a href="#业界方案调研-方案选型" class="headerlink" title="业界方案调研/方案选型"></a>业界方案调研/方案选型</h3><p>具体到前端页面开发来说,前端状态管理工具也有很多,常见的比如各框架自带的 vuex、redux,以及比较热门的 mobx 等,具体的引入可以结合项目自身的情况比如使用的框架、项目技术栈等来进行选型。</p><p>除此之外,有时候我们会遇到一些现有开源工具无法直接在项目中的问题,这种时候我们往往需要“造轮子”,即参考业界成熟的技术方案,结合项目实际情况来调整落地。比如说依赖注入的方案,著名的开源项目中有 Angular 和 VsCode 都实现了依赖注入的框架,但并没有抽离出来直接可用的工具,我们可以通过研究它们的相关代码,分析其中的思路以及实现方式,然后在自己项目中使用。</p><h3 id="架构可拓展性"><a href="#架构可拓展性" class="headerlink" title="架构可拓展性"></a>架构可拓展性</h3><p>个人认为引入新架构或是新技术时,需要考虑两个很重要的点:</p><ol><li>新架构/技术是否能支持业务的未来规划。</li><li>此次引入是否彻底,是否会留下新的技术债务。</li></ol><p>不同的项目或是同一个项目的不同时期,关注的技术点都会不一样。在项目初期,关注重点往往是快速试错与功能迭代;在项目稳定期,项目的维护成本则会逐渐受到重视。</p><p>我们在引入新的技术或架构的时候,还需要考虑项目后续的发展规划。比如说我们在给项目引入依赖注入时,假设我们知道项目后续需要支持以应用中内嵌应用的功能,则可以考虑以 SDK 为维度来进行依赖注入,避免后续在同一个应用中存在两个 SDK 时,依赖注入管理混乱。</p><p>而技术变更会引入的技术债务问题,则还需要在方案设计的时候进行详细评估。举个例子,架构设计的改造,往往产生极大的工作量,面对这样的工作量是否有有效的解决方案,比如引入自动化流程进行、新增人力支援等。如果该问题无法有很好的解决方案,那么引入新技术必定会带来更多的技术债务,这种情况下就需要仔细衡量这个事情值不值得了。</p><p>至于技术方案调研相关,之前有写过一篇更详细的文章:<a href="https://godbasin.github.io/2022/12/03/research-and-design-process/">《技术方案的调研和设计过程》</a>,感兴趣的小伙伴也可以看看。</p><p>实际上,项目复盘也可以很好地解决剩余技术债务的问题,同时还能避免相同的错误再犯,之前<a href="https://godbasin.github.io/2023/03/21/why-project-reviews-are-important/">《为什么项目复盘很重要》</a>一文中也有介绍。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>日子怎么过都是过,过得好过得坏也是一天天过,但稍微对自己有点要求和期待,日子可能会一天天变好呢~</p>]]></content>
<summary type="html">
<p>身为一名程序员,我们经常会调侃自己每天的工作就是在屎山上拉屎。这里的屎山还有一个更好的名称,叫做技术债务。</p>
<h2 id="技术债务是怎么产生的"><a href="#技术债务是怎么产生的" class="headerlink" title="技术债务是怎么产生的"
</summary>
<category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
<category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
</entry>
<entry>
<title>前端性能优化--卡顿链路追踪</title>
<link href="https://godbasin.github.io/2024/10/15/front-end-performance-jank-monitor/"/>
<id>https://godbasin.github.io/2024/10/15/front-end-performance-jank-monitor/</id>
<published>2024-10-15T14:17:25.000Z</published>
<updated>2024-10-15T14:20:06.065Z</updated>
<content type="html"><![CDATA[<p>我们在上一篇<a href="https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/">《前端性能优化–卡顿心跳检测》</a>一文中介绍过基于<code>requestAnimationFrame</code>的卡顿的检测方案实现,这一篇文章我们将会介绍基于该心跳检测方案,要怎么实现链路追踪,来找到产生卡顿的地方。</p><h2 id="卡顿监控实现"><a href="#卡顿监控实现" class="headerlink" title="卡顿监控实现"></a>卡顿监控实现</h2><p>上一篇我们提到的心跳检测,实现的功能很简单,就是卡顿和心跳事件、开始和停止,那么我们卡顿监控使用的时候也比较简单:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="comment">// 心跳 SDK</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">heartBeatMonitor</span>: <span class="title class_">HeartbeatMonitor</span>;</span><br><span class="line"></span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 初始化并绑定事件</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span> = <span class="keyword">new</span> <span class="title class_">HeartbeatMonitor</span>();</span><br><span class="line"> <span class="comment">// PS:此处 addEventListener 为伪代码,可自行实现一个事件转发器</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span>.<span class="title function_">addEventListener</span>(<span class="string">"jank"</span>, <span class="variable language_">this</span>.<span class="property">handleJank</span>);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span>.<span class="title function_">addEventListener</span>(<span class="string">"heartbeat"</span>, <span class="variable language_">this</span>.<span class="property">handleHeartBeat</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 可以初始化的时候就启动</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatMonitor</span>.<span class="title function_">start</span>();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 处理卡顿</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleJank</span>(<span class="params"></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 处理心跳</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleHeartBeat</span>(<span class="params"></span>) {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这时候可以检测到卡顿了,接下来便是在卡顿发生的时候找到问题并上报了。前面<a href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/">《前端性能优化–卡顿的监控和定位》</a>中有大致介绍堆栈的方法,这里我们来介绍下具体要怎么实现吧~</p><h3 id="堆栈追踪卡顿"><a href="#堆栈追踪卡顿" class="headerlink" title="堆栈追踪卡顿"></a>堆栈追踪卡顿</h3><p>同样的,假设我们通过打堆栈的方式来追踪,堆栈信息包括:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">IJankLog</span> {</span><br><span class="line"> <span class="attr">module</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">action</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">logTime</span>: <span class="built_in">number</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么,我们的卡顿检测还需要对外提供<code>log</code>打堆栈的能力:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="comment">// 卡顿链路堆栈</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">jankLogStack</span>: <span class="title class_">IJankLog</span>[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="title function_">log</span>(<span class="params">logPosition: { <span class="variable language_">module</span>: <span class="built_in">string</span>; action: <span class="built_in">string</span> }</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</span>.<span class="title function_">push</span>({</span><br><span class="line"> ...logPosition,</span><br><span class="line"> <span class="attr">logTime</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleHeartBeat</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 心跳的时候,可以将堆栈清空,因为正常心跳发生意味着没有卡顿,此时堆栈内信息可以移除</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</span> = [];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 清空后,添加心跳信息,方便计算耗时</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</span>.<span class="title function_">push</span>({</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...其他内容省略</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当卡顿发生时,我们可以根据堆栈计算出卡顿产生的位置:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">jankLogStack</span>: <span class="title class_">IJankLog</span>[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">handleJank</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> jankPosition = <span class="variable language_">this</span>.<span class="title function_">calculateJankPosition</span>();</span><br><span class="line"> <span class="comment">// 拿到卡顿位置后,可以进行上报</span></span><br><span class="line"> <span class="comment">// PS: reportJank 为伪代码,可以根据项目情况自行实现</span></span><br><span class="line"> <span class="title function_">reportJank</span>(jankPosition);</span><br><span class="line"> <span class="comment">// 打印异常</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class="string">"产生了卡顿,位置信息为:"</span>, jankPosition);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 上报结束后,则需要清空堆栈,继续监听</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">jankLogStack</span> = [];</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...其他内容省略</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面我们来详细看一下,要怎么计算出卡顿产生的位置。</p><h3 id="卡顿位置定位"><a href="#卡顿位置定位" class="headerlink" title="卡顿位置定位"></a>卡顿位置定位</h3><p>我们在代码中,使用<code>log</code>方法来打关键链路日志,那么我们拿到的堆栈信息大概会长这样:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">jankLogStack = [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"拉取数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime1,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"加载数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime2,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Feature 模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"处理数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime3,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染模块"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"渲染数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: logTime4,</span><br><span class="line"> },</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>当卡顿发生的时候,我们可以将堆栈取出来计算最大耗时的位置:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JankMonitor</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">jankLogStack</span>: <span class="title class_">IJankLog</span>[] = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">calculateJankPosition</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 记录产生卡顿的位置</span></span><br><span class="line"> <span class="keyword">let</span> jankPosition;</span><br><span class="line"> <span class="comment">// 记录最大耗时</span></span><br><span class="line"> <span class="keyword">let</span> maxCostTime = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 遍历堆栈,计算每一步耗时</span></span><br><span class="line"> <span class="comment">// 第一个信息为心跳信息,可从第二个开始算起</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">1</span>; i < <span class="variable language_">this</span>.<span class="property">jankLogStack</span>.<span class="property">length</span>; i++) {</span><br><span class="line"> <span class="comment">// 上个位置</span></span><br><span class="line"> <span class="keyword">const</span> previousPosition = <span class="variable language_">this</span>.<span class="property">jankLogStack</span>[i - <span class="number">1</span>];</span><br><span class="line"> <span class="comment">// 当前位置</span></span><br><span class="line"> <span class="keyword">const</span> currentPosition = <span class="variable language_">this</span>.<span class="property">jankLogStack</span>[i];</span><br><span class="line"> <span class="comment">// 链路耗时</span></span><br><span class="line"> <span class="keyword">const</span> costTime = currentPosition.<span class="property">logTime</span> - previousPosition.<span class="property">logTime</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 可以将链路打出来,方便定位</span></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(</span><br><span class="line"> <span class="string">`<span class="subst">${previousPosition.<span class="variable language_">module</span>}</span>-<span class="subst">${previousPosition.action}</span> -> <span class="subst">${currentPosition.<span class="variable language_">module</span>}</span>-<span class="subst">${currentPosition.action}</span>, 耗时 <span class="subst">${costTime}</span> ms`</span></span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 找出最大耗时和最大位置</span></span><br><span class="line"> <span class="keyword">if</span> (costTime > maxCostTime) {</span><br><span class="line"> maxCostTime = costTime;</span><br><span class="line"> jankPosition = {</span><br><span class="line"> ...currentPosition,</span><br><span class="line"> costTime,</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> jankPosition;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...其他内容省略</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样我们就可以计算出产生卡顿时,代码执行的整个链路(需要使用<code>log</code>记录堆栈),同时可找到耗时最大的位置并进行上报。当然,有时候卡顿产生并不只是一个地方,这里也可以调整为将执行超过一定时间的链路全部进行上报。</p><p>现在,我们可以拿到产生卡顿的有效位置,当然前提是需要使用<code>log</code>方法记录关键的链路信息。为了方便,我们可以将其做成一个装饰器来使用。</p><h3 id="jankTrace-装饰器"><a href="#jankTrace-装饰器" class="headerlink" title="@jankTrace 装饰器"></a>@jankTrace 装饰器</h3><p>该装饰器功能很简单,就是调用<code>JankMonitor.log</code>方法:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 装饰器,可用于装饰类中的成员方法和箭头函数</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">JankTrace</span>: <span class="title class_">MethodDecorator</span> | <span class="title class_">PropertyDecorator</span> = <span class="function">(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function"> target,</span></span></span><br><span class="line"><span class="params"><span class="function"> propertyKey,</span></span></span><br><span class="line"><span class="params"><span class="function"> descriptor</span></span></span><br><span class="line"><span class="params"><span class="function"></span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> className = target.<span class="property">constructor</span>.<span class="property">name</span>;</span><br><span class="line"> <span class="keyword">const</span> methodName = propertyKey.<span class="title function_">toString</span>();</span><br><span class="line"> <span class="keyword">const</span> isProperty = !descriptor;</span><br><span class="line"> <span class="keyword">const</span> originalMethod = isProperty</span><br><span class="line"> ? (target <span class="keyword">as</span> <span class="built_in">any</span>)[propertyKey]</span><br><span class="line"> : descriptor.<span class="property">value</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> originalMethod !== <span class="string">"function"</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">"JankTrace decorator can only be applied to methods"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> newFunction = <span class="keyword">function</span> (<span class="params">...args: <span class="built_in">any</span>[]</span>) {</span><br><span class="line"> <span class="comment">// 打印卡顿堆栈</span></span><br><span class="line"> jankMonitor.<span class="title function_">log</span>({</span><br><span class="line"> <span class="attr">moduleValue</span>: className,</span><br><span class="line"> <span class="attr">actionValue</span>: methodName,</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">const</span> syncResult = originalMethod.<span class="title function_">apply</span>(<span class="variable language_">this</span>, args);</span><br><span class="line"> <span class="keyword">return</span> syncResult;</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (isProperty) {</span><br><span class="line"> (target <span class="keyword">as</span> <span class="built_in">any</span>)[propertyKey] = newFunction;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> descriptor!.<span class="property">value</span> = newFunction <span class="keyword">as</span> <span class="built_in">any</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>至此,我们可以直接在一些类方法上去添加装饰器,来实现自动跟踪卡顿链路:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">DataLoader</span> {</span><br><span class="line"> <span class="meta">@JankLog</span></span><br><span class="line"> <span class="title function_">getData</span>(<span class="params"></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="meta">@JankLog</span></span><br><span class="line"> <span class="title function_">loadData</span>(<span class="params"></span>) {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文简单介绍了卡顿检测的一个实现思路,实际上在项目中还有很多其他问题需要考虑,比如需要设置堆栈上限、状态管理等等。</p><p>技术方案在项目中落地时,都需要因地制宜做些调整,来更好地适配自己的项目滴~</p>]]></content>
<summary type="html">
<p>我们在上一篇<a href="https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/">《前端性能优化–卡顿心跳检测》</a>一文中介绍过基于<code>requ
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--JavaScript 数组解构</title>
<link href="https://godbasin.github.io/2024/09/03/front-end-performance-array-performance/"/>
<id>https://godbasin.github.io/2024/09/03/front-end-performance-array-performance/</id>
<published>2024-09-03T13:37:22.000Z</published>
<updated>2024-09-03T13:37:38.763Z</updated>
<content type="html"><![CDATA[<p>之前在给大家介绍性能相关内容的时候,经常说要给大家讲一些更具体的案例,而不是大的解决方案。</p><p>这不,最近刚查到一个数组的性能问题,来给大家分享一下~</p><h2 id="数组解构的性能问题"><a href="#数组解构的性能问题" class="headerlink" title="数组解构的性能问题"></a>数组解构的性能问题</h2><p>ES6 的出现,让前端开发小伙伴们着实高效工作了一番,我们常常会使用解构的方式拼接数组,比如:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 浅拷贝新数组</span></span><br><span class="line"><span class="keyword">const</span> newArray = [...originArray];</span><br><span class="line"><span class="comment">// 拼接数组</span></span><br><span class="line"><span class="keyword">const</span> newArray = [...array1, ...array2];</span><br></pre></td></tr></table></figure><p>这样的代码经常会出现,毕竟对于大多数场景来说,很少会因为这样简单的数组结构导致性能问题。</p><p>但实际上,如果在数据量大的场景下使用,数组解构不仅有性能问题,还可能导致 JavaScript 爆栈等问题。</p><h3 id="两者差异"><a href="#两者差异" class="headerlink" title="两者差异"></a>两者差异</h3><p>使用<code>concat</code>和<code>...</code>拓展符的最大区别是:<code>...</code>使用对象需为可迭代对象,当使用<code>...</code>解构数组时,它会尝试迭代数组的每个元素,并将它们展开到一个新数组中。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line">b = <span class="string">"test"</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a.<span class="title function_">concat</span>(b)); <span class="comment">// [1, 2, 3, 4, 'test']</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>([...a, ...b]);</span><br><span class="line"><span class="comment">// [1, 2, 3, 4, 't', 'e', 's', 't']</span></span><br></pre></td></tr></table></figure><p>如果解构对象不可迭代,则会报错:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line">b = <span class="number">100</span>;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a.<span class="title function_">concat</span>(b)); <span class="comment">// [1, 2, 3, 4, 100]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>([...a, ...b]); <span class="comment">// TypeError: b is not iterable</span></span><br></pre></td></tr></table></figure><p>除此之外,<code>concat()</code>用于在数组末尾添加元素,而<code>...</code>用于在数组的任何位置添加元素:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>];</span><br><span class="line">b = [<span class="number">5</span>, <span class="number">6</span>, <span class="number">7</span>, <span class="number">8</span>];</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(a.<span class="title function_">concat</span>(b)); <span class="comment">// [1, 2, 3, 4, 5, 6, 7, 8]</span></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>([...b, ...a]); <span class="comment">// [5, 6, 7, 8, 1, 2, 3, 4]</span></span><br></pre></td></tr></table></figure><h3 id="性能差异"><a href="#性能差异" class="headerlink" title="性能差异"></a>性能差异</h3><p>由于<code>concat()</code>方法的使用对象为数组,基于次可以进行很多优化,而<code>...</code>拓展符在使用时还需要进行检测和迭代,性能上会是<code>concat()</code>更好。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> big = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e5</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> i, x;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"concat-big"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [].<span class="title function_">concat</span>(big);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"concat-big"</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"spread-big"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [...big];</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"spread-big"</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> a = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> b = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> c = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"><span class="keyword">let</span> d = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">1e3</span>).<span class="title function_">fill</span>(<span class="number">99</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"concat-many"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>].<span class="title function_">concat</span>(a, b, c, d);</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"concat-many"</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">time</span>(<span class="string">"spread-many"</span>);</span><br><span class="line"><span class="keyword">for</span> (i = <span class="number">0</span>; i < <span class="number">1e2</span>; i++) x = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, ...a, ...b, ...c, ...d];</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">timeEnd</span>(<span class="string">"spread-many"</span>);</span><br></pre></td></tr></table></figure><p>上述代码在我的 Chrome 浏览器上输出结果为:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">concat-big: 35.491943359375 ms</span><br><span class="line">spread-big: 268.485107421875 ms</span><br><span class="line">concat-many: 0.55615234375 ms</span><br><span class="line">spread-many: 6.807861328125 ms</span><br></pre></td></tr></table></figure><p>也有网友提供的测试数据为:</p><table><thead><tr><th>浏览器</th><th><code>[...a, ...b]</code></th><th><code>a.concat(b)</code></th></tr></thead><tbody><tr><td>Chrome 113</td><td>350 毫秒</td><td>30 毫秒</td></tr><tr><td>Firefox 113</td><td>400 毫秒</td><td>63 毫秒</td></tr><tr><td>Safari 16.4</td><td>92 毫秒</td><td>71 毫秒</td></tr></tbody></table><p>以及不同数据量的对比数据:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-2.jpg" alt=""></p><p>更多数据可参考<a href="https://jonlinnell.co.uk/articles/spread-operator-performance">How slow is the Spread operator in JavaScript?</a>:</p><h3 id="Array-push-爆栈"><a href="#Array-push-爆栈" class="headerlink" title="Array.push()爆栈"></a><code>Array.push()</code>爆栈</h3><p>当数组数据量很大时,使用<code>Array.push(...array)</code>的组合还可能出现 JavaScript 堆栈溢出的问题,比如这段代码:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> someArray = <span class="keyword">new</span> <span class="title class_">Array</span>(<span class="number">600000</span>).<span class="title function_">fill</span>(<span class="number">1</span>);</span><br><span class="line"><span class="keyword">const</span> newArray = [];</span><br><span class="line"><span class="keyword">let</span> tempArray = [];</span><br><span class="line"></span><br><span class="line">newArray.<span class="title function_">push</span>(...someArray); <span class="comment">// JS error</span></span><br><span class="line">tempArray = newArray.<span class="title function_">concat</span>(someArray); <span class="comment">// can work</span></span><br></pre></td></tr></table></figure><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-1.jpg" alt=""></p><p>这是因为解构会使用<code>apply</code>方法来调用函数,即<code>Array.prototype.push.apply(newArray, someArray)</code>,而参数数量过大时则可能超出堆栈大小,可以这样使用来解决这个问题:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">newArray = [...someArray];</span><br></pre></td></tr></table></figure><h3 id="内存占用"><a href="#内存占用" class="headerlink" title="内存占用"></a>内存占用</h3><p>之前在项目中遇到的特殊场景,两份代码的差异只有数组的创建方式不一致:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-5.jpg" alt=""></p><p>使用<code>newArray = [].concat(oldArray)</code>的时候,内存占用并没有涨,因此不会触发浏览器的 GC:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-3.png" alt=""></p><p>但使用<code>newArray = [...oldArray]</code>解构数组的时候,内存占用会持续增长,因此也会带来频繁的 GC,导致函数执行耗时直线上涨:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/array-performance-4.jpg" alt=""></p><p>可惜的是,对于这个困惑的程度只达到了把该问题修复,但依然无法能建立有效的 demo 复现该问题(因为项目代码过于复杂无法简单提取出可复现 demo)。</p><p>个人认为或许跟前面提到的 JavaScript 堆栈问题有些关系,但目前还没有更多的时间去往底层继续研究,只能在这里小小地记录一下。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://stackoverflow.com/questions/48865710/spread-operator-vs-array-concat">spread operator vs array.concat()</a></li><li><a href="https://jonlinnell.co.uk/articles/spread-operator-performance">How slow is the Spread operator in JavaScript?</a></li></ul><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>今天给大家介绍了一个比较具体的性能问题,可惜没有更完整深入地往下捞到 v8 的实现和内存回收相关的内容,以后有机会有时间的话,可以再翻出来看看叭~</p><p>希望有一天能有机会和能力解答今天的疑惑~</p>]]></content>
<summary type="html">
<p>之前在给大家介绍性能相关内容的时候,经常说要给大家讲一些更具体的案例,而不是大的解决方案。</p>
<p>这不,最近刚查到一个数组的性能问题,来给大家分享一下~</p>
<h2 id="数组解构的性能问题"><a href="#数组解构的性能问题" class="heade
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--任务管理和调度</title>
<link href="https://godbasin.github.io/2024/08/05/front-end-performance-task-schedule/"/>
<id>https://godbasin.github.io/2024/08/05/front-end-performance-task-schedule/</id>
<published>2024-08-05T15:39:12.000Z</published>
<updated>2024-08-05T15:39:40.121Z</updated>
<content type="html"><![CDATA[<p>对于一个前端应用,最理想的性能便是任何用户的交互都不会被阻塞、且能及时得到响应。</p><p>显然,当我们应用程序里需要处理一些大任务计算的时候,这个理想状态是难以达到的。不过,努力去接近也是我们可以尽量去做好的。</p><h1 id="任务调度与性能"><a href="#任务调度与性能" class="headerlink" title="任务调度与性能"></a>任务调度与性能</h1><p>任务调度的出现,基本上是为了更合理地使用和分配资源。在前端应用中,用户的交互则是最高优先级需要响应的,用户操作是否能及时响应,往往是我们衡量一个前端应用是否性能好的重要标准。</p><h2 id="浏览器的“一帧”"><a href="#浏览器的“一帧”" class="headerlink" title="浏览器的“一帧”"></a>浏览器的“一帧”</h2><p>前面在<a href="https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/">《前端性能优化–卡顿心跳检测》</a>一文中,我们提到说使用<code>requestAnimationFrame</code>来检测是否产生了卡顿。除此之外,如果你也处理过简单的异步任务管理(闲时执行等),或许你还用过<code>requestIdleCallback</code>。</p><p>其实,<code>requestAnimationFrame</code>和<code>requestIdleCallback</code>都会在浏览器的每一帧中被执行到。我们来看<a href="https://aerotwist.com/blog/the-anatomy-of-a-frame/">下图</a>:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/anatomy-of-a-frame.svg" alt=""></p><p>每次浏览器渲染的过程顺序为:</p><ol><li>用户事件。</li><li>一个宏任务。</li><li>队列中全部微任务。</li><li><code>requestAnimationFrame</code>。</li><li>浏览器重排/重绘。</li><li><code>requestIdleCallback</code>。</li></ol><p>我们常用的事件监听的顺序则是<a href="https://medium.com/@paul_irish/requestanimationframe-scheduling-for-nerds-9c57f7438ef4">如图</a>:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/life-of-a-frame.jpg" alt=""></p><h2 id="任务切片"><a href="#任务切片" class="headerlink" title="任务切片"></a>任务切片</h2><p>之前在<a href="https://godbasin.github.io/2024/04/03/front-end-performance-long-task/">《让你的长任务在 50 毫秒内结束》</a>一文中说过:RAIL 的目标是在 100 毫秒内完成由用户输入发起的转换,让用户感觉互动是瞬时完成的。</p><p>为确保在 100 毫秒内获得可见响应,RAIL 的准则是在 50 毫秒内处理用户输入事件,这也是为什么我们使用<code>requestIdleCallback</code>处理空闲回调任务时,<code>timeRemaining()</code>有一个 50ms 的上限时间。</p><p>好的任务调度可以让页面不会产生卡顿,这个前提是每个被调度的任务的颗粒度足够细,也可理解为单个任务需要满足下述两个条件之一:</p><ol><li>在 50ms 内执行完成。</li><li>支持暂停以及继续执行。</li></ol><p>对于希望尽可能达到理想状态的系统来说,要让所以可拆卸的任务满足上述条件,都才是最难实现的部分。</p><h2 id="切片后任务执行"><a href="#切片后任务执行" class="headerlink" title="切片后任务执行"></a>切片后任务执行</h2><p>只要任务可控制在 50ms 内结束或者中断再恢复,那么我们就可以很简单地利用浏览器的每一帧渲染过程,来实现“不会产生卡顿”的任务管理。</p><p>最简单的,我们可以设置每一次执行的耗时上限,当每个任务执行完之后,检测一下本次执行耗时,超过 50ms 则通过定时器或是<code>requestAnimationFrame</code>、<code>requestIdleCallback</code>等方法,将剩余任务放到下一次渲染前后处理。</p><p>比如之前<a href="https://godbasin.github.io/2023/09/16/render-engine-calculate-split/">《复杂渲染引擎架构与设计–分片计算》</a>一文中提到的,简单的<code>setTimeout</code>便能使任务执行不阻塞用户操作:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">AsyncCalculateManager</span> {</span><br><span class="line"> <span class="comment">// 每次执行任务的耗时</span></span><br><span class="line"> <span class="keyword">static</span> timeForEveryTask = <span class="number">50</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 跑下一次任务</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">runNext</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">timer</span>) <span class="built_in">clearTimeout</span>(<span class="variable language_">this</span>.<span class="property">timer</span>);</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">timer</span> = <span class="built_in">setTimeout</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// 一个任务跑 50 ms</span></span><br><span class="line"> <span class="keyword">const</span> calculateRange = <span class="variable language_">this</span>.<span class="property">calculateRunner</span>.<span class="title function_">calculateNextTask</span>(</span><br><span class="line"> <span class="title class_">AsyncCalculateManager</span>.<span class="property">timeForEveryTask</span></span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 处理完之后,剩余任务做异步</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">runNext</span>();</span><br><span class="line"> }, <span class="number">10</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>除此之外,<code>requestAnimationFrame</code>适合处理影响页面渲染(比如操作 DOM)的任务,而<code>requestIdleCallback</code>可以处理与页面渲染无关的一些计算任务。</p><p>当然,常见的任务调度还需要支持这些能力:</p><ul><li>定义任务优先级</li><li>并行/串行/顺序执行任务</li></ul><p>在前端应用中,大家都比较认可和熟知的任务调度便是 React 虚拟 DOM 的计算,我们可以来看看。</p><h2 id="React-虚拟-DOM-与任务调度"><a href="#React-虚拟-DOM-与任务调度" class="headerlink" title="React 虚拟 DOM 与任务调度"></a>React 虚拟 DOM 与任务调度</h2><p>React 中使用协调器(Reconciler)与渲染器(Renderer)来优化页面的渲染性能。</p><p>我们都知道在 React 里,可以使用<code>ReactDOM.render</code>/<code>this.setState</code>/<code>this.forceUpdate</code>/<code>useState</code>等方法来触发状态更新,这些方法共用一套状态更新机制,该更新机制主要由两个步骤组成:</p><ol><li>找出变化的组件,每当有更新发生时,协调器会做如下工作:</li></ol><ul><li>调用组件 render 方法将 JSX 转化为虚拟 DOM</li><li>进行虚拟 DOM Diff 并找出变化的虚拟 DOM</li><li>通知渲染器</li></ul><ol start="2"><li>渲染器接到协调器通知,将变化的组件渲染到页面上。</li></ol><p>在 React15 及以前,协调器创建虚拟 DOM 使用的是递归的方式,该过程是无法中断的。这会导致 UI 渲染被阻塞,造成卡顿。为此,React16 中新增了调度器(Scheduler),调度器能够把可中断的任务切片处理,能够调整优先级,重置并复用任务。</p><p>调度器会根据任务的优先级去分配各自的过期时间,在过期时间之前按照优先级执行任务,可以在不影响用户体验的情况下去进行计算和更新。</p><p>简单来说,最重要的依然是两个步骤:</p><ul><li>时间切片:将更新中的 render 阶段划分一个个的小任务,通常来说这些小任务连续执行的最长时间为 5ms</li><li>限制时间执行任务:每次执行小任务,都会记录耗时,如果超过 5ms 就跳出当前任务,并设置一个宏任务开始下一轮事件循环</li></ul><p>通过这样的方式,React 可在浏览器空闲的时候进行调度并执行任务。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://www.qinguanghui.com/react/%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6.html">任务调度 Scheduler</a></li></ul><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>任务调度其实很简单,无非就是将所有执行代码尽可能拆分为一个个的切片任务,并在浏览器每帧渲染前后处理一部分任务,从而达到不阻塞用户操作的目的。</p><p>但实际上这件事要做好来又是很困难的,需要将几乎整个应用程序都搭建于这套任务调度之上,并拆成足够小可执行的任务,往往这才是在项目中做好性能的最大难点。</p>]]></content>
<summary type="html">
<p>对于一个前端应用,最理想的性能便是任何用户的交互都不会被阻塞、且能及时得到响应。</p>
<p>显然,当我们应用程序里需要处理一些大任务计算的时候,这个理想状态是难以达到的。不过,努力去接近也是我们可以尽量去做好的。</p>
<h1 id="任务调度与性能"><a href
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--R 树的使用</title>
<link href="https://godbasin.github.io/2024/07/17/front-end-performance-r-tree/"/>
<id>https://godbasin.github.io/2024/07/17/front-end-performance-r-tree/</id>
<published>2024-07-17T15:01:23.000Z</published>
<updated>2024-07-17T15:01:46.721Z</updated>
<content type="html"><![CDATA[<p>听说程序员里存在一个鄙视链,而前端则在鄙视链的最底端。这是因为以前大多数的前端工作内容都相对简单(或许现在也是如此),在大多数人的眼中,前端只需要写写 HTML 和 CSS,编写页面样式便完成了。</p><p>如今尽管前端的能力越来越强了,涉及到代码构建、编译等,但依然有十分丰富且成熟的工具可供使用,因此前端被认为是可替代性十分强的职位。在降本增效大时代,“前端已死”等说法也常常会被提出来。</p><p>这些说法很多时候是基于前端开发的工作较简单,但实际上并不是所有的开发工作都这么简单的,前端也会有涉及到算法与数据结构的时候。</p><p>今天我们来看看 R-tree 在前端中的应用。</p><h2 id="树的数据结构"><a href="#树的数据结构" class="headerlink" title="树的数据结构"></a>树的数据结构</h2><p>树在前端开发里其实并不应该很陌生,浏览器渲染页面过程中必不可缺,包括 HTML 代码解析完成后得到的 DOM 节点树和 CSS 规则树,布局过程便是通过 DOM 节点树和 CSS 规则树来构造渲染树(Render Tree)。</p><p>基于这样一个渲染过程,我们页面的代码也经常是树的结构进行布局。除此之外,热门前端框架中也少不了 AST 语法树,虚拟 DOM 抽象树等等。</p><h3 id="R-tree"><a href="#R-tree" class="headerlink" title="R-tree"></a>R-tree</h3><p>我们来看一下 <a href="https://zh.wikipedia.org/wiki/R%E6%A0%91">R 树是什么(来自维基百科)</a>:</p><blockquote><p>R 树(R-tree)是用来做空间数据存储的树状数据结构,例如给地理位置,矩形和多边形这类多维数据建立索引。在现实生活中,R 树可以用来存储地图上的空间信息,例如餐馆地址,或者地图上用来构造街道,建筑,湖泊边缘和海岸线的多边形。然后可以用它来回答“查找距离我 2 千米以内的博物馆”,“检索距离我 2 千米以内的所有路段”(然后显示在导航系统中)或者“查找(直线距离)最近的加油站”这类问题。R 树还可以用来加速使用包括大圆距离在内的各种距离度量方式的最邻近搜索。</p></blockquote><p>R 树的核心思想是聚合距离相近的节点,并在树结构的上一层将其表示为这些节点的最小外接矩形,这个最小外接矩形就成为上一层的一个节点。R 树的“R”代表“Rectangle(矩形)”。因为所有节点都在它们的最小外接矩形中,所以跟某个矩形不相交的查询就一定跟这个矩形中的所有节点都不相交。</p><p>一个经典的 R 树结构如下:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/R-tree.svg.png" alt=""></p><p>至于 R 树的算法原理以及复杂度这里就不多介绍了,书上网上都有许多可以供学习的内容参考,我们主要还是介绍算法的应用场景。</p><p>在与图形相关的应用中经常会使用到 R 树,除了上述提到的地图检索以外,图形编辑中也会使用到(检索图形是否发生了碰撞)。</p><p>除此之外,还有在表格场景下,天然适合使用 R 树来管理的数据,主要是范围数据,比如函数依赖的区域范围、条件格式的范围设置、区域权限的范围数据等等。</p><h3 id="Rbush"><a href="#Rbush" class="headerlink" title="Rbush"></a>Rbush</h3><p>前端开发使用 R-tree 的场景大多数是 2D 下,包括上述提到的地图检索、图形碰撞检测、数据可视化、表格区域数据等等。</p><p>虽然我们经常在面试中会问到一些数据结构和算法,甚至有些时候还要求手写出来。但实际上在我们开发的时候,并不需要什么都自己实现一遍。学习算法的目的并不是要完全能自己实现,而是知道在什么场景下使用怎样的算法会更优,因此使用开源稳定的工具也是一种很好的方式。</p><p><a href="/~https://github.com/mourner/rbush">RBush</a> 是一个高性能 JavaScript 库,用于对点和矩形进行 2D 空间索引。它基于优化的 R 树数据结构,支持批量插入。其使用的算法包括:</p><ul><li>单次插入:非递归 R 树插入,最小化 R<em> 树的重叠分割例程(分割在 JS 中非常有效,而其他 R</em> 树修改,如溢出时重新插入和最小化子树重叠搜索,速度太慢,不值得)</li><li>单一删除:使用深度优先树遍历和空时释放策略进行非递归 R 树删除(下溢节点中的条目不会被重新插入,而是将下溢节点保留在树中,只有当其为空时才被删除,这是查询与删除性能之间的良好折衷)</li><li>批量加载:OMT 算法(Overlap Minimizing Top-down Bulk Loading)结合 Floyd–Rivest 选择算法</li><li>批量插入:STLT 算法(小树-大树)</li><li>搜索:标准非递归 R 树搜索</li></ul><p>我们也可以看到,<a href="/~https://github.com/mourner/rbush/blob/master/index.js">整个 Rbush 的实现非常简单</a>,甚至实现代码都没有 demo 和测试代码多。</p><p>使用方式很简单,我们来用个实际场景来使用看看。</p><h3 id="表格区域数据"><a href="#表格区域数据" class="headerlink" title="表格区域数据"></a>表格区域数据</h3><p>表格中使用到区域的地方十分多,前面提到了函数引用区域、条件格式区域、区域权限区域,除此之外还有区域样式、图表区域等等。这些区域因为不会覆盖,支持堆叠、交错,我们在管理的时候使用 R 树来维护,性能会更好。</p><p>基于 Rbush 实现,我们需要定义这个 Rbush 结点的数据。假设我们现有的表格区域数据为:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">ICellRange</span> {</span><br><span class="line"> <span class="attr">startRowIndex</span>: <span class="built_in">number</span>; <span class="comment">// 起始行位置</span></span><br><span class="line"> <span class="attr">endRowIndex</span>: <span class="built_in">number</span>; <span class="comment">// 结束行位置</span></span><br><span class="line"> <span class="attr">startColumnIndex</span>: <span class="built_in">number</span>; <span class="comment">// 起始列位置</span></span><br><span class="line"> <span class="attr">endColumnIndex</span>: <span class="built_in">number</span>; <span class="comment">// 结束列位置</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么每个区域都有对应要存储的数据(<code>data</code>),那么我们可以这么定义我们的 R 树:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">RBush</span> <span class="keyword">from</span> <span class="string">"rbush"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 树节点的数据格式</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">ITreeNode</span><T> {</span><br><span class="line"> <span class="attr">range</span>: <span class="title class_">ICellRange</span>;</span><br><span class="line"> data?: T;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">RTree</span><T> <span class="keyword">extends</span> <span class="title class_">RBush</span><<span class="title class_">ITreeNode</span><T>> {</span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">toBBox</span>(<span class="params">treeNode: ITreeNode<T></span>) {</span><br><span class="line"> <span class="keyword">const</span> { range } = treeNode;</span><br><span class="line"> <span class="comment">// 将单元格范围,转换为 Rbush 范围</span></span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">minX</span>: range.<span class="property">startColumnIndex</span>,</span><br><span class="line"> <span class="attr">maxX</span>: range.<span class="property">endColumnIndex</span>,</span><br><span class="line"> <span class="attr">minY</span>: range.<span class="property">startRowIndex</span>,</span><br><span class="line"> <span class="attr">maxY</span>: range.<span class="property">endRowIndex</span>,</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 需要自行实现的比较</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">compareMinX</span>(<span class="params">treeNode1: ITreeNode<T>, treeNode2: ITreeNode<T></span>) {</span><br><span class="line"> <span class="keyword">return</span> treeNode1.<span class="property">range</span>.<span class="property">startColumnIndex</span> - treeNode2.<span class="property">range</span>.<span class="property">startColumnIndex</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">compareMinY</span>(<span class="params">treeNode1: ITreeNode<T>, treeNode2: ITreeNode<T></span>) {</span><br><span class="line"> <span class="keyword">return</span> treeNode1.<span class="property">range</span>.<span class="property">startRowIndex</span> - treeNode2.<span class="property">range</span>.<span class="property">startRowIndex</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 转换一下数据范围</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">searchTreeNodes</span>(<span class="attr">cellRange</span>: <span class="title class_">ICellRange</span>): <span class="title class_">ITreeNode</span><T>[] {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">search</span>({</span><br><span class="line"> <span class="attr">minX</span>: cellRange.<span class="property">startColumnIndex</span>,</span><br><span class="line"> <span class="attr">maxX</span>: cellRange.<span class="property">endColumnIndex</span>,</span><br><span class="line"> <span class="attr">minY</span>: cellRange.<span class="property">startRowIndex</span>,</span><br><span class="line"> <span class="attr">maxY</span>: cellRange.<span class="property">endRowIndex</span>,</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么,我们表格的许多数据结构都可以基于这个封装了一层的 RTree 来实现。举个区域权限的例子,我们在表格中设置了两个区域权限,显然堆叠部分会需要两个权限都满足才可以编辑:<br><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/auth-range-tree-1.jpg" alt=""></p><p>这样一个查询权限的方法也很简单:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> { <span class="title class_">RTree</span> } <span class="keyword">from</span> <span class="string">"../r-tree"</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 区域权限数据</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">IAuthRangeData</span> {</span><br><span class="line"> <span class="attr">cellRange</span>: <span class="title class_">ICellRange</span>;</span><br><span class="line"> <span class="attr">rangeStatus</span>: <span class="string">"unreadable"</span> | <span class="string">"readonly"</span> | <span class="string">"edit"</span>;</span><br><span class="line"> userIds?: <span class="built_in">string</span>[];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">AuthRangesTree</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="attr">authRangeTree</span>: <span class="title class_">RTree</span><<span class="title class_">IAuthRangeData</span>> = <span class="keyword">new</span> <span class="title class_">RTree</span>(<span class="number">7</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 检索某个用户是否有该区域权限</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">hasRangesAuth</span>(</span><br><span class="line"> <span class="attr">cellRange</span>: <span class="title class_">ICellRange</span>,</span><br><span class="line"> <span class="attr">userId</span>: <span class="built_in">string</span></span><br><span class="line"> ): <span class="title class_">IAuthRangeData</span>[] {</span><br><span class="line"> <span class="keyword">const</span> authRange = <span class="variable language_">this</span>.<span class="property">authRangeTree</span>.<span class="title function_">searchTreeNodes</span>(cellRange);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 若没有设置区域权限,则默认有权限</span></span><br><span class="line"> <span class="keyword">if</span> (!authRange.<span class="property">length</span>) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 若有设置区域权限,则判断是否全满足</span></span><br><span class="line"> <span class="keyword">return</span> !authRange.<span class="title function_">find</span>(<span class="function">(<span class="params">range</span>) =></span> !range.<span class="property">data</span>.<span class="property">userIds</span>.<span class="title function_">includes</span>(userId));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这样,通过使用 R 树来存储数据的方式,我们可以极大地提升页面查询区域权限的性能。毕竟,如果我们只是单纯使用数据的方式去存储,那么每次查询都需要对整个数组遍历并进行碰撞检测,当表格单元格数量达到百万甚至千万时,这个性能问题可不是小事情了。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>前面说过后面会详细介绍一些性能优化的具体例子,本文 R 树的使用便也是其中一个。当然,使用更优的数据结构和算法可以有不少的性能优化,而更多时候我们代码本身编写的问题也经常是导致性能问题的原因,定位并解决这些问题也是零碎但必须解决的事情。</p><p>如果有机会的话,后面看看攒一批代码习惯导致的性能问题,来分享给大家哇。</p>]]></content>
<summary type="html">
<p>听说程序员里存在一个鄙视链,而前端则在鄙视链的最底端。这是因为以前大多数的前端工作内容都相对简单(或许现在也是如此),在大多数人的眼中,前端只需要写写 HTML 和 CSS,编写页面样式便完成了。</p>
<p>如今尽管前端的能力越来越强了,涉及到代码构建、编译等,但依然有
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--卡顿心跳检测</title>
<link href="https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/"/>
<id>https://godbasin.github.io/2024/06/04/front-end-performance-jank-heartbeat-monitor/</id>
<published>2024-06-04T14:00:01.000Z</published>
<updated>2024-06-04T14:00:28.694Z</updated>
<content type="html"><![CDATA[<p>对于重前端计算的网页来说,性能问题天天都冒出来,而操作卡顿可能会直接劝退用户。</p><span id="more"></span><p>前面我们在<a href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/">《前端性能优化–卡顿的监控和定位》</a>一文中介绍过一些卡顿的检测方案,这里我们来讲一下具体的代码实现逻辑好了。</p><h2 id="requestAnimationFrame-心跳检测"><a href="#requestAnimationFrame-心跳检测" class="headerlink" title="requestAnimationFrame 心跳检测"></a>requestAnimationFrame 心跳检测</h2><p>这里我们使用<code>window.requestAnimationFrame</code>来作为检测卡顿的核心机制。</p><p>前面也有说过,<code>requestAnimationFrame()</code>会在浏览器下次重绘之前调用,60Hz 的电脑显示器每秒钟<code>requestAnimationFrame</code>会被执行 60 次。</p><p>那么,我们可以简单地判断,假设两次<code>requestAnimationFrame</code>之间的执行耗时超过一定值,则可以认为浏览器的重绘被阻塞了,页面响应产生了卡顿,这里我们将该值设置为 1s:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">HeartbeatMonitor</span> {</span><br><span class="line"> <span class="comment">// 上一次心跳的时间</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">preHeartBeatTime</span>: <span class="built_in">number</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">checkNextTick</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">preHeartBeatTime</span> = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="title function_">requestAnimationFrame</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> currentTime = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="comment">// 取出执行耗时</span></span><br><span class="line"> <span class="keyword">let</span> timeDistance = currentTime - <span class="variable language_">this</span>.<span class="property">preHeartBeatTime</span>;</span><br><span class="line"> <span class="comment">// 超过 1s 则认为是卡顿了</span></span><br><span class="line"> <span class="keyword">if</span> (timeDistance > <span class="number">1000</span>) {</span><br><span class="line"> <span class="comment">// 注:dispatchEvent 为伪代码,具体可自行实现</span></span><br><span class="line"> <span class="comment">// 对外抛事件表示发生了卡顿</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">dispatchEvent</span>(<span class="string">'jank'</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 对外抛事件表示为普通心跳</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">dispatchEvent</span>(<span class="string">'heartbeat'</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 继续下一次检测</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">checkNextTick</span>();</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过这种方式,我们简单判断代码执行是否产生了卡顿。当然,我们在实际使用的时候,还需要提供开启和停止检测的能力:</p><h3 id="启动和停止检测"><a href="#启动和停止检测" class="headerlink" title="启动和停止检测"></a>启动和停止检测</h3><p>已知<code>requestAnimationFrame</code>的返回值是一个请求 ID,用于唯一标识回调列表中的条目,可以使用<code>window.cancelAnimationFrame()</code>来取消刷新回调请求,因此我们可以基于此开实现启动和停止检测的能力:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">HeartbeatMonitor</span> {</span><br><span class="line"> <span class="comment">// 上一次心跳的时间</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">preHeartBeatTime</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="comment">// 心跳定时器</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">heartBeatTimer</span>: <span class="built_in">number</span> | <span class="literal">null</span> = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 开启卡顿监控</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="title function_">start</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">heartBeatTimer</span>) <span class="variable language_">this</span>.<span class="title function_">checkNextTick</span>();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 结束卡顿监控</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="title function_">stop</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 取消 requestAnimationFrame</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">heartBeatTimer</span>) <span class="title function_">cancelAnimationFrame</span>(<span class="variable language_">this</span>.<span class="property">heartBeatTimer</span>);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatTimer</span> = <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">checkNextTick</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">preHeartBeatTime</span> = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatTimer</span> = <span class="title function_">requestAnimationFrame</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> currentTime = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="comment">// 取出执行耗时</span></span><br><span class="line"> <span class="keyword">let</span> timeDistance = currentTime - <span class="variable language_">this</span>.<span class="property">preHeartBeatTime</span>;</span><br><span class="line"> <span class="comment">// 超过 1s 则认为是卡顿了</span></span><br><span class="line"> <span class="keyword">if</span> (timeDistance > <span class="number">1000</span>) {</span><br><span class="line"> <span class="comment">// 注:dispatchEvent 为伪代码,具体可自行实现</span></span><br><span class="line"> <span class="comment">// 对外抛事件表示发生了卡顿</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">dispatchEvent</span>(<span class="string">'jank'</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 对外抛事件表示为普通心跳</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">dispatchEvent</span>(<span class="string">'heartbeat'</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 继续下一次检测</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">checkNextTick</span>();</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当然,对于有状态的运行期,最好我们还可以给其加上一个状态位标志,来避免重复调用、外界获取状态等情况,不过这个很简单,大家可以自行实现。</p><h3 id="页面隐藏"><a href="#页面隐藏" class="headerlink" title="页面隐藏"></a>页面隐藏</h3><p>由于<code>requestAnimationFrame</code>基于页面的绘制来执行回调的,当我们页面被切走之后,显然不会触发回调,那么可能存在一个问题:此时检测的耗时很可能会超出卡顿阈值。</p><p>因此,我们还需要对页面是否被切走的场景做处理,最简单莫过于页面切走之后就停止,切回来再打开:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">HeartbeatMonitor</span> {</span><br><span class="line"> <span class="comment">// 上一次心跳的时间</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">preHeartBeatTime</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="comment">// 心跳定时器</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">heartBeatTimer</span>: <span class="built_in">number</span> | <span class="literal">null</span> = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">document</span>.<span class="title function_">addEventListener</span>(<span class="string">'visibilitychange'</span>, <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">document</span>.<span class="property">visibilityState</span> === <span class="string">"hidden"</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">stop</span>();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">start</span>();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> } </span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 开启卡顿监控</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="title function_">start</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">heartBeatTimer</span>) <span class="variable language_">this</span>.<span class="title function_">checkNextTick</span>();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 结束卡顿监控</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="title function_">stop</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 取消 requestAnimationFrame</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">heartBeatTimer</span>) <span class="title function_">cancelAnimationFrame</span>(<span class="variable language_">this</span>.<span class="property">heartBeatTimer</span>);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatTimer</span> = <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">checkNextTick</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">preHeartBeatTime</span> = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">heartBeatTimer</span> = <span class="title function_">requestAnimationFrame</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> currentTime = <span class="title class_">Date</span>.<span class="title function_">now</span>();</span><br><span class="line"> <span class="comment">// 取出执行耗时</span></span><br><span class="line"> <span class="keyword">let</span> timeDistance = currentTime - <span class="variable language_">this</span>.<span class="property">preHeartBeatTime</span>;</span><br><span class="line"> <span class="comment">// 超过 1s 则认为是卡顿了</span></span><br><span class="line"> <span class="keyword">if</span> (timeDistance > <span class="number">1000</span>) {</span><br><span class="line"> <span class="comment">// 注:dispatchEvent 为伪代码,具体可自行实现</span></span><br><span class="line"> <span class="comment">// 对外抛事件表示发生了卡顿</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">dispatchEvent</span>(<span class="string">'jank'</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 对外抛事件表示为普通心跳</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">dispatchEvent</span>(<span class="string">'heartbeat'</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 继续下一次检测</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">checkNextTick</span>();</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>现在我们实现了卡顿的检测,但是基于此我们只能得到页面在运行过程中是否产生了卡顿,但是难以定位卡顿的问题出现在哪。前面<a href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/">《前端性能优化–卡顿的监控和定位》</a>一文中有大致介绍堆栈的方法,我们下一篇来说一下基于当前的<code>HeartbeatMonitor</code>来看看怎么实现。</p><p>主要是分两篇来讲的话,我就可以偷个懒啦:)</p>]]></content>
<summary type="html">
<p>对于重前端计算的网页来说,性能问题天天都冒出来,而操作卡顿可能会直接劝退用户。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--用户卡顿检测</title>
<link href="https://godbasin.github.io/2024/05/02/front-end-performance-jank-detect/"/>
<id>https://godbasin.github.io/2024/05/02/front-end-performance-jank-detect/</id>
<published>2024-05-02T15:35:25.000Z</published>
<updated>2024-05-02T15:35:30.458Z</updated>
<content type="html"><![CDATA[<p>前面跟大家介绍过<a href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/">前端性能卡顿的检测和监控</a>,其中提到了<code>requestAnimationFrame</code>心跳检测等方式来检测代码执行耗时,从而判断是否存在卡顿。</p><p>而实际上我们观察一些用户反馈,会发现这样检测的效果并不是很理想。</p><h1 id="用户感觉的“卡”"><a href="#用户感觉的“卡”" class="headerlink" title="用户感觉的“卡”"></a>用户感觉的“卡”</h1><p>一般来说,我们会根据代码检测的任务耗时超过一定值判断为卡顿,比如超过 1s 的长任务。但实际上,这样的方法难以准确命中“用户侧卡顿”的场景,这是因为:</p><ul><li>超过 1s 的任务执行时,用户未必在进行页面操作,未感受到“卡顿”</li><li>对用户来说,在浏览器中各个过程中的卡顿阈值是不一致的,比如:<ul><li>页面打开过程中,会习惯性地等待,此时卡顿阈值会稍微高一些</li><li>页面加载完成后,对各种功能的操作响应更敏感,希望能快速响应操作</li></ul></li></ul><p>因此,我们可以重新定义卡顿指标,可以将其分为两种:</p><ol><li>技术侧卡顿(代码长任务)。</li><li>用户侧卡顿(交互响应耗时)。</li></ol><p>本文我们重点来探讨用户侧卡顿的检测。</p><h2 id="用户侧卡顿"><a href="#用户侧卡顿" class="headerlink" title="用户侧卡顿"></a>用户侧卡顿</h2><p>如果你有认真整理用户反馈,便会发现,对于大型应用比如在线表格/网页游戏等,相比于加载过程中偶尔一两秒的卡顿,更让他们难以接受的问题有频繁出现卡顿、某个操作卡顿耗时过长、某个较频繁的操作必现卡顿等。</p><p>那么,我们可以基于这些场景,重新定义用户侧卡顿的指标,满足以下场景均可认为产生了卡顿:</p><table><thead><tr><th>问题</th><th>对应性能指标</th><th>指标定义</th><th>补充说明</th></tr></thead><tbody><tr><td>操作后响应不及时</td><td>用户交互(点击)后,rAF 响应耗时 > 1000ms</td><td>交互卡顿</td><td>类似 INP(参考 <a href="https://web.dev/articles/inp),但滚动行为考虑在内">https://web.dev/articles/inp),但滚动行为考虑在内</a></td></tr><tr><td>操作(编辑/滚动)频繁出现卡顿</td><td>20s 内,交互响应卡顿次数 > 5</td><td>交互卡顿频率</td><td></td></tr><tr><td>某个操作卡顿耗时过长,长达 5s/10s 甚至更多</td><td>- 交互响应卡顿耗时 > 5s</td></tr><tr><td>- 交互响应卡顿耗时 > 10s</td><td>交互长耗时卡顿</td><td></td></tr><tr><td>某个较频繁的操作必现卡顿</td><td>相同的卡顿埋点次数 > 5</td><td>同因交互卡顿</td></tr></tbody></table><p>这里有一个难处理的地方:如何判断用户交互后产生了卡顿呢?因为我们可以拆分成以下情况:</p><ol><li>用户交互后,同步执行长耗时任务产生卡顿。</li><li>用户交互后,异步执行逻辑的时候产生卡顿。</li></ol><h3 id="1-同步任务卡顿"><a href="#1-同步任务卡顿" class="headerlink" title="1. 同步任务卡顿"></a>1. 同步任务卡顿</h3><p>我们可以在监听到用户交互时进行耗时计算:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">"click"</span>, <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> startTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"> <span class="title function_">requestAnimationFrame</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> duringTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>() - startTime;</span><br><span class="line"> <span class="comment">// 交互后超过 1s 才响应</span></span><br><span class="line"> <span class="keyword">if</span> (duringTime > <span class="number">1000</span>) {</span><br><span class="line"> <span class="comment">// 则判断为卡顿</span></span><br><span class="line"> }</span><br><span class="line"> }, <span class="number">0</span>);</span><br><span class="line">});</span><br></pre></td></tr></table></figure><h3 id="2-异步任务卡顿"><a href="#2-异步任务卡顿" class="headerlink" title="2. 异步任务卡顿"></a>2. 异步任务卡顿</h3><p>对于异步任务,由于卡顿发生在用户交互后,难以通过代码直接发现。我们可以从另外一个角度分析,即当页面交互发生卡顿时,用户常常会在页面中进行操作,来确认页面是否无响应。因此,我们可以通过这样的代码判断:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> clickCount = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">let</span> hasClick = <span class="literal">false</span>;</span><br><span class="line"><span class="variable language_">window</span>.<span class="title function_">addEventListener</span>(<span class="string">"click"</span>, <span class="function">() =></span> {</span><br><span class="line"> clickCount++;</span><br><span class="line"> <span class="keyword">if</span> (hasClick) <span class="keyword">return</span>;</span><br><span class="line"> hasClick = <span class="literal">true</span>;</span><br><span class="line"> <span class="built_in">setTimeout</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// 卡顿过程中发生了连续点击操作</span></span><br><span class="line"> <span class="keyword">if</span> (clickCount > <span class="number">2</span>) {</span><br><span class="line"> <span class="comment">// 则判断为卡顿</span></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 清空数据</span></span><br><span class="line"> clickCount = <span class="number">0</span>;</span><br><span class="line"> hasClick = <span class="literal">false</span>;</span><br><span class="line"> }, <span class="number">0</span>);</span><br><span class="line">});</span><br></pre></td></tr></table></figure><h2 id="总卡顿指标设计"><a href="#总卡顿指标设计" class="headerlink" title="总卡顿指标设计"></a>总卡顿指标设计</h2><p>综上所述,我们会将以下情况作为一次卡顿的产生,并且做卡顿次数的上报:</p><ul><li>用户交互后,同步卡顿超过 1s</li><li>检测到一次宏任务中,用户连续点击操作超过 5 次</li></ul><p>同时,我们可以在特特定场景发生的时候,将数据以及日志同时进行上报,比如:</p><ul><li>20s 内产生卡顿次数 > 5</li><li>检测到某段代码执行超过 5s/10s</li><li>检测到卡顿埋点中卡顿(超过 1s)的相同埋点多次产生(相同的卡顿埋点次数 > 5)</li></ul><p>通过这样的方式,我们可以判断用户是否产生了卡顿。但实际上要如何定位卡顿的位置呢,还是得通过日志和埋点进行,可以参考<a href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/">《前端性能优化–卡顿的监控和定位》</a>一文。</p><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>很多时候,我们开发在实现功能的时候,常常会从编程出发去思考问题,但实际上我们可以更贴近用户一些滴~</p>]]></content>
<summary type="html">
<p>前面跟大家介绍过<a href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/">前端性能卡顿的检测和监控</a>,其中提到了<code>requestAn
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>让你的长任务在 50 毫秒内结束</title>
<link href="https://godbasin.github.io/2024/04/03/front-end-performance-long-task/"/>
<id>https://godbasin.github.io/2024/04/03/front-end-performance-long-task/</id>
<published>2024-04-03T12:28:02.000Z</published>
<updated>2024-04-03T12:42:50.038Z</updated>
<content type="html"><![CDATA[<p>虽然之前有跟大家分享过不少卡顿相关的内容,实际上网页里卡顿的产生基本上都是由于长任务导致的。当然,能阻塞用户操作的,我们说的便是主线程上的长任务。</p><p>浏览器中的长任务可能是 JavaScript 的编译、解析 HTML 和 CSS、渲染页面,或者是我们编写的 JavaScript 中产生了长任务导致。</p><h1 id="让你的长任务保持在-50-ms-内"><a href="#让你的长任务保持在-50-ms-内" class="headerlink" title="让你的长任务保持在 50 ms 内"></a>让你的长任务保持在 50 ms 内</h1><p>之前在介绍<a href="https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/">前端性能优化–卡顿篇</a>时,提到可以将大任务进行拆解:</p><blockquote><p>考虑将任务执行耗时控制在 50 ms 左右。每执行完一个任务,如果耗时超过 50 ms,将剩余任务设为异步,放到下一次执行,给到页面响应用户操作和更新渲染的时间。</p></blockquote><p>为什么是 50 毫秒呢?</p><p>这个数值并不是随便写的,主要来自于 Google 员工开发的 <a href="https://web.dev/articles/rail">RAIL 模型</a>。</p><h2 id="RAIL-模型"><a href="#RAIL-模型" class="headerlink" title="RAIL 模型"></a>RAIL 模型</h2><p>RAIL 表示 Web 应用生命周期的四个不同方面:<strong>响应(Response)</strong>、<strong>动画(Animation)</strong>、<strong>空闲(Idel)</strong>和<strong>加载(Load)</strong>。由于用户对每种情境有不同的性能预期,因此,系统会根据情境以及关于用户如何看待延迟的用户体验调研来确定效果目标。</p><p>人机交互学术研究由来已久,在 <a href="https://www.nngroup.com/articles/response-times-3-important-limits/">Jakob Nielsen’s work on response time limits</a> 中提出三个阈值:</p><ul><li>100 毫秒:大概是让用户感觉系统立即做出反应的极限,这意味着除了显示结果之外不需要特殊的反馈</li><li>1 秒:大概是用户思想流保持不间断的极限,即使用户会注意到延迟。一般情况下,大于 0.1 秒小于 1.0 秒的延迟不需要特殊反馈,但用户确实失去了直接操作数据的感觉</li><li>10 秒:大概是让用户的注意力集中在对话上的极限。对于较长的延迟,用户会希望在等待计算机完成的同时执行其他任务,因此应该向他们提供反馈,指示计算机预计何时完成。如果响应时间可能变化很大,则延迟期间的反馈尤其重要,因为用户将不知道会发生什么。</li></ul><p>在此基础上,如今机器性能都有大幅度的提升,因此基于用户的体验,RAIL 增加了一项:</p><ul><li>0-16 ms:大概是用户感受到流畅的动画体验的数值。只要每秒渲染 60 帧,这类动画就会感觉很流畅,也就是每帧 16 毫秒(包括浏览器将新帧绘制到屏幕上所需的时间),让应用生成一帧大约 10 毫秒</li></ul><p>由于这篇文章我们讨论的是长任务相关,因此主要考虑生命周期中的响应(Response),目标便是要求 100 毫秒内获得可见响应。</p><h2 id="在-50-毫秒内处理事件"><a href="#在-50-毫秒内处理事件" class="headerlink" title="在 50 毫秒内处理事件"></a>在 50 毫秒内处理事件</h2><p>RAIL 的目标是在 100 毫秒内完成由用户输入发起的转换,让用户感觉互动是瞬时完成的。</p><p>目标是 100 毫秒,但是页面运行时除了输入处理之外,通常还会执行其他工作,并且这些工作会占用可用于获得可接受输入响应的部分时间。</p><p>因此,为确保在 100 毫秒内获得可见响应,RAIL 的准则是在 50 毫秒内处理用户输入事件:</p><blockquote><p>为确保在 100 毫秒内获得可见响应,请在 50 毫秒内处理用户输入事件。这适用于大多数输入,例如点击按钮、切换表单控件或启动动画。这不适用于轻触拖动或滚动。</p></blockquote><p>除了响应之外,RAIL 对其他的生命周期也提出了对应的准则,总体为:</p><ul><li>响应(Response):在 50 毫秒内处理事件</li><li>动画(Animation):在 10 毫秒内生成一帧</li><li>空闲(Idel):最大限度地延长空闲时间</li><li>加载(Load):提交内容并在 5 秒内实现互动</li></ul><p>具体每个行为的目标和准则是如何考虑和确定的,大家可以自行学习,这里不再赘述。</p><h1 id="长任务优化"><a href="#长任务优化" class="headerlink" title="长任务优化"></a>长任务优化</h1><p>网页加载时,长时间任务可能会占用主线程,使页面无法响应用户输入(即使页面看起来已就绪)。点击和点按通常不起作用,因为尚未附加事件监听器、点击处理程序等。</p><p>基于前面介绍的 RAIL 模型,我们可以将超过 50 毫秒的任务称之为长任务,即:任何连续不间断的且主 UI 线程繁忙 50 毫秒及以上的时间区间。</p><p>实际上,Chrome 浏览器中的 Performance 面板也是如此定义的,我们录制一段 Performance,当主线程同步执行的任务超过 50 毫秒时,该任务块会被标记为红色。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/a-devtools-performance-pa-938d4fa393ba4_1440.png" alt=""></p><h2 id="识别长任务"><a href="#识别长任务" class="headerlink" title="识别长任务"></a>识别长任务</h2><p>一般来说,在前端网页中容易出现的长任务包括:</p><ul><li>大型的 JavaScript 代码加载</li><li>解析 HTML 和 CSS</li><li>DOM 查询/DOM 操作</li><li>运算量较大的 JavaScript 脚本的执行</li></ul><h3 id="使用-Chrome-Devtools"><a href="#使用-Chrome-Devtools" class="headerlink" title="使用 Chrome Devtools"></a>使用 Chrome Devtools</h3><p>我们可以在 Chrome 开发者工具中,通过录制 Performance 的方式,手动查找时长超过 50 毫秒的脚本的“长红/黄色块”,然后分析这些任务块的执行内容,来识别出长任务。</p><p>我们可以选择 Bottom-Up 和 Group by Activity 面板来分析这些长任务(关于如何使用 Performance 面板,可以参考<a href="https://developer.chrome.com/docs/devtools/performance?hl=zh-cn">分析运行时性能</a>一文):</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/selecting-long-task-lab-acf1b77536fe5_1440.png" alt=""></p><p>比如在上图中,导致任务耗时较长的原因是一组成本高昂的 DOM 查询。</p><h3 id="使用-Long-Tasks-API"><a href="#使用-Long-Tasks-API" class="headerlink" title="使用 Long Tasks API"></a>使用 Long Tasks API</h3><p>我们还可以使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceLongTaskTiming">Long Tasks API</a> 来确定哪些任务导致互动延迟:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="keyword">function</span> (<span class="params">list</span>) {</span><br><span class="line"> <span class="keyword">const</span> perfEntries = list.<span class="title function_">getEntries</span>();</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i < perfEntries.<span class="property">length</span>; i++) {</span><br><span class="line"> <span class="comment">// 分析长任务</span></span><br><span class="line"> }</span><br><span class="line">}).<span class="title function_">observe</span>({ <span class="attr">entryTypes</span>: [<span class="string">"longtask"</span>] });</span><br></pre></td></tr></table></figure><h3 id="识别大型脚本"><a href="#识别大型脚本" class="headerlink" title="识别大型脚本"></a>识别大型脚本</h3><p>大型脚本通常是导致耗时较长的任务的主要原因,我们可以想办法来识别。</p><p>除了使用上述的方法,我们还可以使用<code>PerformanceObserver</code>识别:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">resource</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> entries = resource.<span class="title function_">getEntries</span>();</span><br><span class="line"></span><br><span class="line"> entries.<span class="title function_">forEach</span>(<span class="function">(<span class="params">entry: PerformanceResourceTiming</span>) =></span> {</span><br><span class="line"> <span class="comment">// 获取 JavaScript 资源</span></span><br><span class="line"> <span class="keyword">if</span> (entry.<span class="property">initiatorType</span> !== <span class="string">"script"</span>) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">const</span> startTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">window</span>.<span class="title function_">requestAnimationFrame</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// JavaScript 资源加载完成</span></span><br><span class="line"> <span class="keyword">const</span> endTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"> <span class="comment">// 如果此时耗时大于 50 ms,则可任务出现了长任务</span></span><br><span class="line"> <span class="keyword">const</span> isLongTask = endTime - startTime > <span class="number">50</span>;</span><br><span class="line"> });</span><br><span class="line"> });</span><br><span class="line">}).<span class="title function_">observe</span>({ <span class="attr">entryTypes</span>: [<span class="string">"resource"</span>] });</span><br></pre></td></tr></table></figure><p>这种方式我们还可以通过<code>entry.name</code>拿到对应的加载资源,针对性地进行处理。</p><h3 id="自定义性能指标"><a href="#自定义性能指标" class="headerlink" title="自定义性能指标"></a>自定义性能指标</h3><p>除此之外,我们还可以通过在代码中埋点,自行计算执行耗时,从而针对可预见的场景识别出长任务:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 可预见的大任务执行前打点</span></span><br><span class="line">performance.<span class="title function_">mark</span>(<span class="string">"bigTask:start"</span>);</span><br><span class="line"><span class="keyword">await</span> <span class="title function_">doBigTask</span>();</span><br><span class="line"><span class="comment">// 执行后打点</span></span><br><span class="line">performance.<span class="title function_">mark</span>(<span class="string">"bigTask:end"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 测量该任务</span></span><br><span class="line">performance.<span class="title function_">measure</span>(<span class="string">"bigTask"</span>, <span class="string">"bigTask:start"</span>, <span class="string">"bigTask:end"</span>);</span><br></pre></td></tr></table></figure><p>再配合<code>PerformanceObserver</code>获取对应的性能数据,大于 50 毫秒则可以判断为长任务、</p><h2 id="优化长任务"><a href="#优化长任务" class="headerlink" title="优化长任务"></a>优化长任务</h2><p>发现长任务之后,我们就可以进行对应的长任务优化。</p><h3 id="过大的-JavaScript-脚本"><a href="#过大的-JavaScript-脚本" class="headerlink" title="过大的 JavaScript 脚本"></a>过大的 JavaScript 脚本</h3><p>大型脚本通常是导致耗时较长的任务的主要原因,尤其是首屏加载时尽量避免加载不必要的代码。</p><p>我们可以考虑拆分这些脚本:</p><ol><li>首屏加载,仅加载必要的最小 JavaScript 代码。</li><li>其他 JavaScript 代码进行模块化,进行分包加载。</li><li>通过预加载、闲时加载等方式,完成剩余所需模块的代码加载。</li></ol><p>拆分 JavaScript 脚本,使得用户打开页面时,只发送初始路由所需的代码。这样可以最大限度地减少需要解析和编译的脚本量,从而缩短网页加载时,也有助于提高 First Input Delay (FID) 和 Interaction to Next Paint (INP) 时间。</p><p>有很多工具可以帮助我们完成这项工作:</p><ul><li><a href="https://webpack.js.org/guides/code-splitting/">webpack</a></li><li><a href="https://parceljs.org/code_splitting.html">Parcel</a></li><li><a href="https://rollupjs.org/guide/en#dynamic-import">Rollup</a></li></ul><p>这些热门的模块打包器,都支持动态加载的方式来拆分 JavaScript 脚本。我们甚至可以限制每个构建模块的大小,来防止某个模块的 JavaScript 脚本过大,具体的使用方式大家可以自行搜索。</p><h3 id="过长的-JavaScript-执行任务"><a href="#过长的-JavaScript-执行任务" class="headerlink" title="过长的 JavaScript 执行任务"></a>过长的 JavaScript 执行任务</h3><p>主线程一次只能处理一个任务。如果任务的延时时间超过某一点(确切来说是 50 毫秒),则会被归类为耗时较长的任务。</p><p>对于这种过长的执行任务,优化方案也十分直接:<strong>任务拆分</strong>,直观来看就是这样:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/a-single-long-task-versus-724bb5ecd4b3f_1440.png" alt=""></p><p>一般来说,任务拆分可以分为两种:</p><ol><li>串行执行的不同执行任务。</li><li>单个超大的执行任务。</li></ol><h4 id="串行任务的拆分"><a href="#串行任务的拆分" class="headerlink" title="串行任务的拆分"></a>串行任务的拆分</h4><p>对于串行执行的不同任务,可以将不同任务的调用从同步改成异步即可,比如 <a href="https://web.dev/articles/optimize-long-tasks">Optimize long tasks</a> 这篇文章中详细介绍的:</p><p><code>saveSettings()</code>的函数,该函数会调用五个函数来完成某些工作:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">saveSettings</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="title function_">validateForm</span>();</span><br><span class="line"> <span class="title function_">showSpinner</span>();</span><br><span class="line"> <span class="title function_">saveToDatabase</span>();</span><br><span class="line"> <span class="title function_">updateUI</span>();</span><br><span class="line"> <span class="title function_">sendAnalytics</span>();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/the-savesettings-function-b71e8e42d8bf7_1440.png" alt=""></p><p>对这些串行任务进行拆分有很多种方式,比如:</p><ul><li>使用<code>setTimeOut()</code>/<code>postTask()</code>实现异步</li><li>自行实现任务管理器,管理串行任务执行,每执行一个任务后释放主线程,再执行下一个任务(还需考虑优先级执行任务)</li></ul><p>具体的代码可以参考 <a href="https://web.dev/articles/optimize-long-tasks">Optimize long tasks</a> 该文章,理想的优化效果为:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/the-same-savesettings-fun-689035655ea7a_1440.png" alt=""></p><h4 id="单个超大任务的拆分"><a href="#单个超大任务的拆分" class="headerlink" title="单个超大任务的拆分"></a>单个超大任务的拆分</h4><p>有时候我们的应用中需要做大量的运算,比如对上百万个数据做一系列的计算,此时我们可以考虑进行分批拆分。</p><p>拆分的时候需要注意几个事情:</p><ol><li>尽量将每个小任务拆成 50 毫秒左右的执行时间。</li><li>大任务分批执行,会由同步执行变为异步执行,需要考虑中间态(是否有新的任务插入,是否会重复执行)。</li></ol><p>之前在介绍复杂渲染引擎的时候,有详细讲解使用分批计算的方法进行性能优化,具体可以参考<a href="https://godbasin.github.io/2023/09/16/render-engine-calculate-split/">《复杂渲染引擎架构与设计–5.分片计算》</a>一文。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://web.dev/articles/rail">Measure performance with the RAIL model</a></li><li><a href="https://web.dev/articles/reduce-javascript-payloads-with-code-splitting">Reduce JavaScript payloads with code splitting</a></li><li><a href="https://web.dev/articles/preload-critical-assets">Preload critical assets to improve loading speed</a></li><li><a href="https://web.dev/articles/long-tasks-devtools">Are long JavaScript tasks delaying your Time to Interactive?</a></li><li><a href="https://web.dev/articles/optimize-long-tasks">Optimize long tasks</a></li></ul><h1 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h1><p>对于大型复杂的前端应用来说,卡顿和长任务都是家常便饭。</p><p>性能优化没有捷径,有的都是一步步定位,一点点分析,一处处解决。每一个问题都是独立的问题,但我们还可以识别它们的共性,提供更高效的解决路径。</p>]]></content>
<summary type="html">
<p>虽然之前有跟大家分享过不少卡顿相关的内容,实际上网页里卡顿的产生基本上都是由于长任务导致的。当然,能阻塞用户操作的,我们说的便是主线程上的长任务。</p>
<p>浏览器中的长任务可能是 JavaScript 的编译、解析 HTML 和 CSS、渲染页面,或者是我们编写的 J
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--数据指标体系</title>
<link href="https://godbasin.github.io/2024/03/17/front-end-performance-metric/"/>
<id>https://godbasin.github.io/2024/03/17/front-end-performance-metric/</id>
<published>2024-03-17T13:28:33.000Z</published>
<updated>2024-03-17T13:28:20.908Z</updated>
<content type="html"><![CDATA[<p>常常进行前端性能优化的小伙伴们会发现,实际开发中性能优化总是阶段性的:页面加载很慢/卡顿 -> 性能优化 -> 堆叠需求 -> 加载慢/卡顿 -> 性能优化。</p><p>这是因为我们的项目往往也是阶段性的:快速功能开发 -> 出现性能问题 -> 优化性能 -> 快速功能开发。</p><p>建立一个完善的性能指标体系,便可以在需求开发阶段发现页面性能的下降,及时进行修复。</p><h2 id="前端性能指标体系"><a href="#前端性能指标体系" class="headerlink" title="前端性能指标体系"></a>前端性能指标体系</h2><p>为什么需要进行性能优化呢?这是因为一个快速响应的网页可以有效降低用户访问的跳出率,提升网页的留存率,从而收获更多的用户。参考<a href="https://web.dev/case-studies/economic-times-cwv?hl=zh-cn">《经济时报》如何超越核心网页指标阈值,并使跳出率总体提高了 43%</a>,这个例子中主要优化了两个指标:Largest Contentful Paint (LCP) 和 Cumulative Layout Shift (CLS)。</p><p>除此之外,页面速度是一个重要的搜索引擎排名因素,它影响到你的网页是否能被更多用户访问。</p><h3 id="常见的前端性能指标"><a href="#常见的前端性能指标" class="headerlink" title="常见的前端性能指标"></a>常见的前端性能指标</h3><p>我们来看下常见的前端性能指标,由于网页的响应速度往往包含很多方面(页面内容出现、用户可操作、流畅度等等),因此性能数据也由不同角度的指标组成:</p><ul><li><a href="https://web.dev/articles/fcp?hl=zh-cn">First Contentful Paint (FCP)</a>:首次内容绘制,衡量从网页开始加载到网页任何部分呈现在屏幕上所用的时间</li><li><a href="https://web.dev/articles/lcp?hl=zh-cn">Largest Contentful Paint (LCP)</a>:最大内容绘制,衡量从网页开始加载到屏幕上渲染最大的文本块或图片元素所用的时间</li><li><a href="https://web.dev/articles/fid?hl=zh-cn">First Input Delay (FID)</a>:首次输入延迟,衡量从用户首次与您的网站互动(点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器实际能够响应该互动的时间</li><li><a href="https://web.dev/articles/inp?hl=zh-cn">Interaction to Next Paint (INP)</a>:衡量与网页进行每次点按、点击或键盘交互的延迟时间,并根据互动次数选择该网页最差的互动延迟时间(或接近最高延迟时间)作为单个代表性值,以描述网页的整体响应速度</li><li><a href="https://web.dev/articles/tti?hl=zh-cn">Time to Interactive (TTI)</a>:可交互时间,衡量的是从网页开始加载到视觉呈现、其初始脚本(若有)已加载且能够快速可靠地响应用户输入的时间</li><li><a href="https://web.dev/articles/tbt?hl=zh-cn">Total Blocking Time (TBT)</a>:总阻塞时间,测量 FCP 和 TTI 之间的总时间,在此期间,主线程处于屏蔽状态的时间够长,足以阻止输入响应</li><li><a href="https://web.dev/articles/cls?hl=zh-cn">Cumulative Layout Shift (CLS)</a>:衡量从页面开始加载到其生命周期状态更改为隐藏之间发生的所有意外布局偏移的累计得分</li><li><a href="https://web.dev/articles/ttfb?hl=zh-cn">Time to First Byte (TTFB)</a>:首字节时间,测量网络使用资源的第一个字节响应用户请求所需的时间</li></ul><p>这些是 <a href="https://web.dev/articles/user-centric-performance-metrics">User-centric performance metrics</a> 中介绍到的指标,其中 FCP、LCP、FID、INP/TTI 在我们常见的前端开发中会比较经常用到。</p><p>最简单的,一般前端应用都会关心以下几个指标:</p><ol><li>FCP/LCP,该指标影响内容呈现给用户的体验,对页面跳出率影响最大。</li><li>FID/INP,该指标影响用户与网页交互的体验,对功能转化率和网页留存率影响较大。</li><li>TTI,该指标也为前端网页常用指标,页面可交互即用户可进行操作了。</li></ol><p>除了这些简单的指标外,我们要如何建立起对网页完整的性能指标呢?一套成熟又完善的解决方案为 Google 的 <a href="https://developers.google.com/speed/docs/insights/v5/about">PageSpeed Insights (PSI) </a>。</p><h3 id="PageSpeed-Insights-PSI"><a href="#PageSpeed-Insights-PSI" class="headerlink" title="PageSpeed Insights (PSI)"></a>PageSpeed Insights (PSI)</h3><p>PageSpeed Insights (PSI) 是一项免费的 Google 服务,可报告网页在移动设备和桌面设备上的用户体验,并提供关于如何改进网页的建议。</p><p>前面在<a href="https://godbasin.github.io/2020/08/29/front-end-performance-analyze/">《补齐Web前端性能分析的工具盲点》</a>一文中,我们简单介绍过 Google 的另外一个服务–<a href="https://developer.chrome.com/docs/lighthouse/overview">Lighthouse</a>。</p><p>PageSpeed Insights 和 Lighthouse 的区别主要为:</p><table><thead><tr><th>特征</th><th>PageSpeed Insights</th><th>Lighthouse</th></tr></thead><tbody><tr><td>如何访问</td><td><a href="https://pagespeed.web.dev/">https://pagespeed.web.dev/</a>(浏览器访问;无需登录)</td><td><a href="https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk">Google Chrome 浏览器扩展</a>(推荐非开发人员使用)<br /> <a href="https://developer.chrome.com/docs/lighthouse/overview/#devtools">Chrome DevTools</a> <br /> <a href="https://developer.chrome.com/docs/lighthouse/overview/#cli">Node CLI 工具</a> <br /> <a href="/~https://github.com/GoogleChrome/lighthouse">Lighthouse CI</a></td></tr><tr><td>数据来源</td><td>Chrome 用户体验报告(真实数据)<br />Lighthouse API(模拟实验室数据)</td><td>Lighthouse API</td></tr><tr><td>评估</td><td>一次一页</td><td>一次一页或一次多页</td></tr><tr><td>指标</td><td>核心网络生命、页面速度性能指标(首次内容绘制、速度指数、最大内容绘制、交互时间、总阻塞时间、累积布局偏移)</td><td>性能(包括页面速度指标)、可访问性、最佳实践、SEO、渐进式 Web 应用程序(如果适用)</td></tr><tr><td>建议</td><td>标有<code>Opportunities and Diagnostics</code>的部分提供了提高页面速度的具体建议。</td><td>标有<code>Opportunities and Diagnostics</code>的部分提供了提高页面速度的具体建议。堆栈包可用于定制改进建议。</td></tr></tbody></table><p>简单来说,PageSpeed Insights 可同时获取实验室性能数据和用户实测数据,而 Lighthouse 则可获取实验室性能数据以及网页整体优化建议(包括但不限于性能建议)。</p><p><a href="https://godbasin.github.io/2020/08/29/front-end-performance-analyze/">我们之前提到过</a>,前端性能监控包括两种方式:合成监控(Synthetic Monitoring,SYN)、真实用户监控(Real User Monitoring,RUM)。这两种监控的性能数据,便是分别对应着实验室数据和用户实测数据。</p><p>实测数据是通过监控访问网页的所有用户,并针对其中每个用户的各自的体验,衡量一组给定的性能指标来确定的。和实验室数据不同,由于现场数据基于真实用户访问数据,因此它反映了用户的实际设备、网络条件和用户的地理位置。</p><p>当然,实测数据也可以由用户真实访问页面时进行上报收集,稍微大一点的前端应用都会这么做。但在此之前,如果你的前端网页没有做数据上报监控,也可以使用 PageSpeed Insights 工具进行简单的测试。但考虑到 PageSpeed Insights 收集的用户皆基于 Chrome 浏览器(CrUX),且需要登录的应用无法有效地获取真实数据,那么自行搭建一套性能指标体系则是最好的。</p><p>虽然实际上 PageSpeed Insights 服务并不能解决我们所有的问题,但是我们可以参考它的性能指标,来搭建自己的性能体系呀。</p><h3 id="核心网页指标"><a href="#核心网页指标" class="headerlink" title="核心网页指标"></a>核心网页指标</h3><p>参考 Google 的 <a href="https://developers.google.com/speed/docs/insights/v5/about">PageSpeed Insights</a>,我们知道 PSI 会报告真实用户在上一个 28 天收集期内的 First Contentful Paint (FCP)、First Input Delay (FID)、Largest Contentful Paint (LCP)、Cumulative Layout Shift (CLS) 和 Interaction to Next Paint (INP) 体验,同时 PSI 还报告了实验性指标首字节时间 (TTFB) 的体验。</p><p>其中,核心网页指标包括 FID/INP、LCP 和 CLS。</p><h4 id="FID"><a href="#FID" class="headerlink" title="FID"></a>FID</h4><p><a href="https://web.dev/articles/fid">First Input Delay (FID)</a> 衡量的是从用户首次与网页互动(即,点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器能够实际开始处理事件处理脚本以响应该互动的时间。</p><p>我们可以使用 <a href="https://wicg.github.io/event-timing">Event Timing API</a> 在 JavaScript 中衡量 FID:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">entryList</span>) =></span> {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.<span class="title function_">getEntries</span>()) {</span><br><span class="line"> <span class="keyword">const</span> delay = entry.<span class="property">processingStart</span> - entry.<span class="property">startTime</span>;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'FID candidate:'</span>, delay, entry);</span><br><span class="line"> }</span><br><span class="line">}).<span class="title function_">observe</span>({<span class="attr">type</span>: <span class="string">'first-input'</span>, <span class="attr">buffered</span>: <span class="literal">true</span>});</span><br></pre></td></tr></table></figure><p>实际上,从 2024 年 3 月开始,FID 将替换为 Interaction to Next Paint (INP),后面我们会着重介绍。</p><h4 id="LCP"><a href="#LCP" class="headerlink" title="LCP"></a>LCP</h4><p><a href="https://web.dev/articles/lcp">Largest Contentful Paint (LCP)</a> 指标会报告视口内可见的最大图片或文本块的呈现时间(相对于用户首次导航到页面的时间)。</p><p>我们可以使用 <a href="https://wicg.github.io/largest-contentful-paint/">Largest Contentful Paint API</a> 在 JavaScript 中测量 LCP: </p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">entryList</span>) =></span> {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.<span class="title function_">getEntries</span>()) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'LCP candidate:'</span>, entry.<span class="property">startTime</span>, entry);</span><br><span class="line"> }</span><br><span class="line">}).<span class="title function_">observe</span>({<span class="attr">type</span>: <span class="string">'largest-contentful-paint'</span>, <span class="attr">buffered</span>: <span class="literal">true</span>});</span><br></pre></td></tr></table></figure><h4 id="CLS"><a href="#CLS" class="headerlink" title="CLS"></a>CLS</h4><p>许多网站都面临布局不稳定的问题:DOM 元素由于内容异步加载而发生移动。</p><p><a href="https://web.dev/articles/cls">Cumulative Layout Shift (CLS)</a> 指标便是用来衡量在网页的整个生命周期内发生的每次意外布局偏移的最大突发布局偏移分数。我们可以从<code>Layout Instability</code>方法中获得布局偏移:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">addEventListener</span>(<span class="string">"load"</span>, <span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable constant_">DCLS</span> = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">list</span>) =></span> {</span><br><span class="line"> list.<span class="title function_">getEntries</span>().<span class="title function_">forEach</span>(<span class="function">(<span class="params">entry</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (entry.<span class="property">hadRecentInput</span>)</span><br><span class="line"> <span class="keyword">return</span>; <span class="comment">// Ignore shifts after recent input.</span></span><br><span class="line"> <span class="variable constant_">DCLS</span> += entry.<span class="property">value</span>;</span><br><span class="line"> });</span><br><span class="line"> }).<span class="title function_">observe</span>({<span class="attr">type</span>: <span class="string">"layout-shift"</span>, <span class="attr">buffered</span>: <span class="literal">true</span>});</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>布局偏移分数是该移动两个测量的乘积:影响比例和距离比例。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">layout shift score = impact fraction * distance fraction</span><br></pre></td></tr></table></figure><h4 id="Interaction-to-Next-Paint-INP"><a href="#Interaction-to-Next-Paint-INP" class="headerlink" title="Interaction to Next Paint (INP)"></a>Interaction to Next Paint (INP)</h4><p>FID 仅在用户首次与网页互动时报告响应情况。尽管第一印象很重要,但首次互动不一定代表网页生命周期内的所有互动。此外,FID 仅测量首次互动的“输入延迟”部分,即浏览器在开始处理互动之前必须等待的时间(由于主线程繁忙)。</p><p><a href="https://web.dev/articles/inp">Interaction to Next Paint (INP)</a> 用于通过观察用户在访问网页期间发生的所有符合条件的互动的延迟时间,评估网页对用户互动的总体响应情况。</p><p>INP 不仅会衡量首次互动,还会考虑所有互动,并报告网页整个生命周期内最慢的互动。此外,INP 不仅会测量延迟部分,还会测量从互动开始,一直到事件处理脚本,再到浏览器能够绘制下一帧的完整时长。因此是 Interaction to Next Paint。这些实现细节使得 INP 能够比 FID 更全面地衡量用户感知的响应能力。</p><p>从 2024 年 3 月开始,INP 将替代 FID 加入 Largest Contentful Paint (LCP) 和 Cumulative Layout Shift (CLS),作为三项稳定的核心网页指标。</p><p>INP 的计算方法是观察用户与网页进行的所有互动,而互动是指在同一逻辑用户手势触发的一组事件处理脚本。例如,触摸屏设备上的“点按”互动包括多个事件,如<code>pointerup</code>、<code>pointerdown</code>和<code>click</code>。互动可由 JavaScript、CSS、内置浏览器控件(例如表单元素)或由以上各项驱动。</p><p>我们同样可以使用 <a href="https://wicg.github.io/event-timing">Event Timing API</a> 在 JavaScript 中衡量 FID:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">entryList</span>) =></span> {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.<span class="title function_">getEntries</span>()) {</span><br><span class="line"> <span class="keyword">const</span> delay = entry.<span class="property">processingStart</span> - entry.<span class="property">startTime</span>;</span><br><span class="line"> }</span><br><span class="line">}).<span class="title function_">observe</span>({<span class="attr">type</span>: <span class="string">'event'</span>, <span class="attr">buffered</span>: <span class="literal">true</span>});</span><br></pre></td></tr></table></figure><p>关于 INP 的优化,可以参考 <a href="https://web.dev/articles/optimize-inp">Optimize Interaction to Next Paint</a>。</p><h4 id="web-vitals-JavaScript-库"><a href="#web-vitals-JavaScript-库" class="headerlink" title="web-vitals JavaScript 库"></a>web-vitals JavaScript 库</h4><p><a href="/~https://github.com/GoogleChrome/web-vitals">web-vitals JavaScript 库</a> 使用<code>PerformanceObserver</code>,用于测量真实用户的所有 Web Vitals 指标,其方式准确匹配 Chrome 的测量方式,提供了上述提到的各种指标数据:CLS、FID、LCP、INP、FCP、TTFB。</p><p>我们可以使用 web-vitals 库来收集到所需的数据。</p><h3 id="评估体验质量"><a href="#评估体验质量" class="headerlink" title="评估体验质量"></a>评估体验质量</h3><p>PSI 根据网页指标计划设置了阈值,将用户体验质量分为三类:良好、需要改进或较差,具体可参考 <a href="https://developers.google.com/speed/docs/insights/v5/about?hl=zh-cn">PageSpeed Insights 简介</a>。</p><p>值得注意的是,PSI 报告所有指标的第 75 百分位。</p><p>为便于开发者了解其网站上最令人沮丧的用户体验,选择第 75 百分位。通过应用上述相同阈值,这些字段指标值被归类为良好/需要改进/欠佳。</p><p>这与我们常见的前端性能指标监控不大一样,因为一般来说大家会取平均值来评估指标。而取 75 百分位这种方式,值得我们去好好思考哪种计算方式更能真实反应用户的体验。</p><p>当然,上述 PSI 的性能指标体系,也未必完全适合我们网页使用,我们还可以针对网页的实际情况做出调整。举个例子,网页的 FCP/LCP 虽然十分影响用户的留存,但如果是对于专注服务于老用户、操作频繁、使用时长长的应用来说,网页运行过程中的流畅性更值得关注。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://web.dev/articles/lab-and-field-data-differences">Why lab and field data can be different (and what to do about it)</a></li><li><a href="https://web.dev/blog/inp-cwv">Advancing Interaction to Next Paint</a></li><li><a href="https://developers.google.com/speed/docs/insights/mobile?hl=zh-cn">在 PageSpeed Insights 中针对网站进行移动设备浏览体验分析</a></li><li><a href="/~https://github.com/w3c/longtasks/blob/main/loaf-explainer.md">Long Animation Frames (LoAF)</a></li><li><a href="https://web.dev/articles/user-centric-performance-metrics?hl=zh-cn">以用户为中心的效果指标</a></li><li><a href="https://web.dev/articles/smoothness">Towards an animation smoothness metric</a></li></ul><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>性能优化的事项很多,事情也往往很杂。当我们去针对我们网页进行性能优化事项的时候,如何评估我们的成果也是一个永恒不变的话题。</p><p>建立起有效的性能指标体系,就能更直观地展示出网页存在的性能问题,以及优化后的效果。</p><p>但需要注意的是,一味地追求指标数据并不都是一件好事情,因为为了指标好看往往我们会牺牲掉一些其他的体验。最终在平衡取舍下,呈现给用户最合适的体验才是开发的责任所在。</p>]]></content>
<summary type="html">
<p>常常进行前端性能优化的小伙伴们会发现,实际开发中性能优化总是阶段性的:页面加载很慢/卡顿 -&gt; 性能优化 -&gt; 堆叠需求 -&gt; 加载慢/卡顿 -&gt; 性能优化。</p>
<p>这是因为我们的项目往往也是阶段性的:快速功能开发 -&gt; 出现性能问题
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>有趣的 PerformanceObserver</title>
<link href="https://godbasin.github.io/2024/02/21/front-end-performance-about-performanceobserver/"/>
<id>https://godbasin.github.io/2024/02/21/front-end-performance-about-performanceobserver/</id>
<published>2024-02-21T14:12:23.000Z</published>
<updated>2024-02-21T14:13:06.937Z</updated>
<content type="html"><![CDATA[<p>之前在研究小伙伴遗留代码的时候,发现了<code>PerformanceObserver</code>这玩意,不看不知道,越看越有意思。</p><p>其实这个 API 出了挺久了,机缘巧合下一直没有接触到,直到最近开始深入研究前端性能情况。</p><h2 id="PerformanceObserver"><a href="#PerformanceObserver" class="headerlink" title="PerformanceObserver"></a>PerformanceObserver</h2><p>其实单看<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver/PerformanceObserver"><code>PerformanceObserver</code>的官方描述</a>,好像没什么特别的:</p><blockquote><p><code>PerformanceObserver()</code>构造函数使用给定的观察者<code>callback</code>生成一个新的<code>PerformanceObserver</code>对象。当通过<code>observe()</code>方法注册的条目类型的性能条目事件被记录下来时,调用该观察者回调。</p></blockquote><p>乍一看,好像跟我们网页开发和性能数据没什么太大关系。</p><h3 id="常见的性能指标数据获取"><a href="#常见的性能指标数据获取" class="headerlink" title="常见的性能指标数据获取"></a>常见的性能指标数据获取</h3><p>在很早的时候,前端开发的性能数据很多都是从<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Performance"><code>Performance</code></a>里获取:</p><blockquote><p><code>Performance</code>接口可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、User Timing API 和 Resource Timing API。</p></blockquote><p>提到页面加载耗时,还是得祭出这张熟悉的图(来自<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceNavigationTiming">PerformanceNavigationTiming API</a>):</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_6.png" alt=""></p><p>上述图中的数据都可以从<code>window.performance</code>中获取到。</p><p>一般来说,我们可以在页面加载的某个结点(比如<code>onload</code>)的时候获取,并进行上报。</p><p>但这仅包含页面打开过程的性能数据,而近年来除了网页打开,网页使用过程中的用户体验也逐渐开始被重视了起来。</p><p>2024 年 3 月起,INP (Interaction to Next Paint) 将替代 First Input Delay (FID) 加入 Largest Contentful Paint (LCP) 和 Cumulative Layout Shift (CLS),作为三项稳定的核心网页指标。尽管第一印象很重要,但首次互动(FID)不一定代表网页生命周期内的所有互动(INP)。</p><p>这意味着我们还需要关注整个网页生命周期内的用户体验,<code>PerformanceObserver</code>的设计正是为了提供用户体验相关性能数据,它鼓励开发人员尽可能使用。</p><h3 id="PerformanceObserver-对象"><a href="#PerformanceObserver-对象" class="headerlink" title="PerformanceObserver 对象"></a>PerformanceObserver 对象</h3><p>[<code>PerformanceObserver</code>]{<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver}">https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver}</a> 对象为性能监测对象,用于监测性能度量事件,在浏览器的性能时间轴记录新的 performance entry 的时候将会被通知。</p><p>研究过前端性能的人,或许还有些对<code>PerformanceObserver</code>不大熟悉(比如我),但是所有大概都知道 Chrome 浏览器的 Performance 性能时间轴:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/front-end-performance-analyze_5.jpg" alt=""></p><p>作为 Performance 面板的老用户,我们常常会从时间轴上捞取出存在性能问题的操作,然后细细分析和研究对应的代码执行情况。而这个时间轴上记录下 performance entry 时,我们可以当通过<code>observe()</code>方法获取到对应的内容和数据。</p><p>前面提到,如果我们需要关注网页在整个生命周期中的性能情况,意味着需要定期轮询、埋点等方式做上报。通过使用<code>PerformanceObserver</code>接口,我们可以:</p><ul><li>避免轮询时间线来检测新指标</li><li>避免新增删除重复数据逻辑来识别新指标</li><li>避免与其他可能想要操纵缓冲区的消费者的竞争条件</li></ul><h3 id="PageSpeed-Insights-PSI-前端性能指标"><a href="#PageSpeed-Insights-PSI-前端性能指标" class="headerlink" title="PageSpeed Insights (PSI) 前端性能指标"></a>PageSpeed Insights (PSI) 前端性能指标</h3><p>之前给大家讲过<a href="">前端性能数据指标体系</a>,我们能看到核心网页指标包括 FID、LCP 和 CLS,他们都可以从使用<code>PerformanceObserver</code>直接拿到:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// FID</span></span><br><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">entryList</span>) =></span> {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.<span class="title function_">getEntries</span>()) {</span><br><span class="line"> <span class="keyword">const</span> delay = entry.<span class="property">processingStart</span> - entry.<span class="property">startTime</span>;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"FID candidate:"</span>, delay, entry);</span><br><span class="line"> }</span><br><span class="line">}).<span class="title function_">observe</span>({ <span class="attr">type</span>: <span class="string">"first-input"</span>, <span class="attr">buffered</span>: <span class="literal">true</span> });</span><br><span class="line"><span class="comment">// LCP</span></span><br><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">entryList</span>) =></span> {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> entryList.<span class="title function_">getEntries</span>()) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"LCP candidate:"</span>, entry.<span class="property">startTime</span>, entry);</span><br><span class="line"> }</span><br><span class="line">}).<span class="title function_">observe</span>({ <span class="attr">type</span>: <span class="string">"largest-contentful-paint"</span>, <span class="attr">buffered</span>: <span class="literal">true</span> });</span><br></pre></td></tr></table></figure><p>此外,<a href="/~https://github.com/GoogleChrome/web-vitals">web-vitals JavaScript 库</a>可用来测量真实用户的所有 Web Vitals 指标,其方式准确匹配 Chrome 的测量方式。他提供了 PSI 中的各种指标数据:CLS、FID、LCP、INP、FCP、TTFB,如果你仔细研究它的实现,便是使用<code>PerformanceObserver</code>的能力。</p><p>比如,INP 需要监控整个网页生命周期中的交互体验,我们可以看到<a href="/~https://github.com/GoogleChrome/web-vitals/blob/2301de5015e82b09925238a228a0893635854587/src/onINP.ts#L202">其实现</a>基于<code>PerformanceEventTiming</code>的监测实现:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">list</span>) =></span> {</span><br><span class="line"> list.<span class="title function_">getEntries</span>().<span class="title function_">forEach</span>(<span class="function">(<span class="params">entry</span>) =></span> {</span><br><span class="line"> <span class="comment">// Full duration</span></span><br><span class="line"> <span class="keyword">const</span> duration = entry.<span class="property">duration</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Input delay (before processing event)</span></span><br><span class="line"> <span class="keyword">const</span> delay = entry.<span class="property">processingStart</span> - entry.<span class="property">startTime</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Synchronous event processing time</span></span><br><span class="line"> <span class="comment">// (between start and end dispatch)</span></span><br><span class="line"> <span class="keyword">const</span> eventHandlerTime = entry.<span class="property">processingEnd</span> - entry.<span class="property">processingStart</span>;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Total duration: <span class="subst">${duration}</span>`</span>);</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Event delay: <span class="subst">${delay}</span>`</span>);</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Event handler duration: <span class="subst">${eventHandlerTime}</span>`</span>);</span><br><span class="line"> });</span><br><span class="line">}).<span class="title function_">observe</span>({ <span class="attr">type</span>: <span class="string">"event"</span> });</span><br></pre></td></tr></table></figure><p>而<code>Event Timing API</code>中包括的用户交互事件几乎是很全的,但该方式可用于检测用户交互的流畅性,并不能作为出现卡顿时的定位方案。具体卡顿的定位,可参考<a href="">《前端性能卡顿的监控和定位方案》</a>一文。</p><h3 id="resource-observe-获取资源加载时机"><a href="#resource-observe-获取资源加载时机" class="headerlink" title="resource observe 获取资源加载时机"></a>resource observe 获取资源加载时机</h3><p>在<a href="">《前端性能卡顿的监控和定位方案》</a>这篇文章中,我们还发现一个有意思的使用方式:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">resource</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> entries = resource.<span class="title function_">getEntries</span>();</span><br><span class="line"></span><br><span class="line"> entries.<span class="title function_">forEach</span>(<span class="function">(<span class="params">entry: PerformanceResourceTiming</span>) =></span> {</span><br><span class="line"> <span class="comment">// 获取 JavaScript 资源</span></span><br><span class="line"> <span class="keyword">if</span> (entry.<span class="property">initiatorType</span> !== <span class="string">"script"</span>) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">const</span> startTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">window</span>.<span class="title function_">requestAnimationFrame</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// JavaScript 资源加载完成</span></span><br><span class="line"> <span class="keyword">const</span> endTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"> });</span><br><span class="line"> });</span><br><span class="line">}).<span class="title function_">observe</span>({ <span class="attr">entryTypes</span>: [<span class="string">"resource"</span>] });</span><br></pre></td></tr></table></figure><p>除了使用<code>performanceObserver</code>监测<code>resource</code>资源获取性能数据,我们还可以在回调触发时开始计数,以此计算该 JavaScript 资源加载耗时,从而考虑是否需要对资源进行更合理的分包。</p><h3 id="自定义性能指标"><a href="#自定义性能指标" class="headerlink" title="自定义性能指标"></a>自定义性能指标</h3><p>配合<code>PerformanceObserver</code>,我们还可以使用<a href="https://w3c.github.io/user-timing/"><code>User Timing API</code></a> 进行自定义打点:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Record the time immediately before running a task.</span></span><br><span class="line">performance.<span class="title function_">mark</span>(<span class="string">"myTask:start"</span>);</span><br><span class="line"><span class="keyword">await</span> <span class="title function_">doMyTask</span>();</span><br><span class="line"><span class="comment">// Record the time immediately after running a task.</span></span><br><span class="line">performance.<span class="title function_">mark</span>(<span class="string">"myTask:end"</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// Measure the delta between the start and end of the task</span></span><br><span class="line">performance.<span class="title function_">measure</span>(<span class="string">"myTask"</span>, <span class="string">"myTask:start"</span>, <span class="string">"myTask:end"</span>);</span><br></pre></td></tr></table></figure><p>然后使用<code>PerformanceObserver</code>获取相关指标数据:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 有兼容性,需要处理异常</span></span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">const</span> po = <span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">list</span>) =></span> {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> entry <span class="keyword">of</span> list.<span class="title function_">getEntries</span>()) {</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(entry.<span class="title function_">toJSON</span>());</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> <span class="comment">// 监测 measure entry</span></span><br><span class="line"> po.<span class="title function_">observe</span>({ <span class="attr">type</span>: <span class="string">"measure"</span>, <span class="attr">buffered</span>: <span class="literal">true</span> });</span><br><span class="line">} <span class="keyword">catch</span> (e) {}</span><br></pre></td></tr></table></figure><p>更多的使用方式,可以参考<a href="https://web.dev/articles/custom-metrics?hl=zh-cn">自定义指标</a>一文。</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><ul><li><a href="https://w3c.github.io/user-timing">User Timing Level 3</a></li><li><a href="https://w3c.github.io/performance-timeline">Performance Timeline</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming">PerformanceEventTiming</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure">Performance: measure() method</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceEntry/entryType">PerformanceEntry.entryType</a></li><li><a href="https://developer.chrome.com/docs/devtools/performance/timeline-reference">Timeline event reference</a></li></ul><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>由于<code>PerformanceObserver</code> 对象与浏览器的性能时间轴紧紧相关,基于此我们可以做很多性能监测的事情。</p><p>如果想偷懒,使用 web-vitals JavaScript 库并对 PSI 定义的核心指标进行上报,我们就能大概掌握了网页的核心性能指标数据,并以此进行分析和优化。</p><p>前端性能在前端领域中,也算是个亘古不变的难题,每次研究总能学到新的知识,这也是挺有趣的一件事呢。</p>]]></content>
<summary type="html">
<p>之前在研究小伙伴遗留代码的时候,发现了<code>PerformanceObserver</code>这玩意,不看不知道,越看越有意思。</p>
<p>其实这个 API 出了挺久了,机缘巧合下一直没有接触到,直到最近开始深入研究前端性能情况。</p>
<h2 id="Per
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>前端性能优化--卡顿的监控和定位</title>
<link href="https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/"/>
<id>https://godbasin.github.io/2024/01/21/front-end-performance-no-response-solution/</id>
<published>2024-01-21T13:21:06.000Z</published>
<updated>2024-01-21T13:21:11.390Z</updated>
<content type="html"><![CDATA[<p>卡顿大概是前端遇到的问题的最棘手的一个,尤其是卡顿产生的时候常常无法进行其他操作,甚至控制台也打开不了。</p><span id="more"></span><p>但是这活落到了咱们头上,老板说啥就得做啥。能本地复现的我们还能打开控制台,打个断点或者录制 Performance 来看看到底哪些地方占用较大的耗时。如果没法本地复现呢?</p><h2 id="卡顿检测"><a href="#卡顿检测" class="headerlink" title="卡顿检测"></a>卡顿检测</h2><p>首先,我们来看看可以怎么主动检测卡顿的出现。</p><p>卡顿,顾名思义则是代码执行产生长耗时,导致浏览器无法及时响应用户的操作。那么,我们可以基于不同的方案,来监测当前页面响应的延迟。</p><h3 id="Worker-心跳方案"><a href="#Worker-心跳方案" class="headerlink" title="Worker 心跳方案"></a>Worker 心跳方案</h3><p>对应浏览器来说,由于 JavaScript 是单线程的设计,当卡顿发生的时候,往往是由于 JavaScript 在执行过长的逻辑,常见于大量数据的遍历操作,甚至是进入死循环。</p><p>利用这个特效,我们可以在页面打开的时候,就启动一个 Worker 线程,使用心跳的方式与主线程进行同步。假设我们希望能监测 1s 以上的卡顿,我们可以设置主线程每间隔 1s 向 Worker 发送心跳消息。(当然,线程通讯本身需要一些耗时,且 JavaScript 的计时器也未必是准时的,因此心跳需要给予一定的冗余范围)</p><p>由于页面发生卡顿的时候,主线程往往是忙碌状态,我们可以通过 Worker 里丢失心跳的时候进行上报,就能及时发现卡顿的产生。</p><p>但是其实 Worker 更多时候用于检测网页崩溃,用来检测卡顿的效果其实还不如使用<code>window.requestAnimationFrame</code>,因为线程通信的耗时和延迟导致该方案不大准确。</p><h3 id="window-requestAnimationFrame-方案"><a href="#window-requestAnimationFrame-方案" class="headerlink" title="window.requestAnimationFrame 方案"></a>window.requestAnimationFrame 方案</h3><p>前面<a href="https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/">前端性能优化–卡顿篇</a>有简单提到一些卡顿的检测方案,市面上大多数的方案也是基于<code>window.requestAnimationFrame</code>方法来检测是否有卡顿出现。</p><p><code>window.requestAnimationFrame()</code>会在浏览器下次重绘之前调用,常常用来更新动画。这是因为<code>setTimeout</code>/<code>setInterval</code>计时器只能保证将回调添加至浏览器的回调队列(宏任务)的时间,不能保证回调队列的运行时间,因此使用<code>window.requestAnimationFrame</code>会更合适。</p><p>通常来说,大多数电脑显示器的刷新频率是 60Hz,也就是说每秒钟<code>window.requestAnimationFrame</code>会被执行 60 次。因此可以使用<code>window.requestAnimationFrame</code>来监控卡顿,具体的方案会依赖于我们项目的要求。</p><p>比如,有些人会认为<a href="https://zhuanlan.zhihu.com/p/39292837">连续出现 3 个低于 20 的 FPS 即可认为网页存在卡顿</a>,这种情况下我们则针对这个数值进行上报。</p><p>除此之外,假设我们认为页面中存在超过特定时间(比如 1s)的长耗时任务即存在明显卡顿,则我们可以判断两次<code>window.requestAnimationFrame</code>执行间超过一定时间,则发生了卡顿。</p><p>使用<code>window.requestAnimationFrame</code>监测卡顿需要注意的是,他是一个被十分频繁执行的代码,不应该处理过多的逻辑。</p><h3 id="Long-Tasks-API-方案"><a href="#Long-Tasks-API-方案" class="headerlink" title="Long Tasks API 方案"></a>Long Tasks API 方案</h3><p>熟悉前端性能优化的开发都知道,阻塞主线程达 50 毫秒或以上的任务会导致以下问题:</p><ul><li>可交互时间(TTI)延迟</li><li>严重不稳定的交互行为 (轻击、单击、滚动、滚轮等) 延迟</li><li>严重不稳定的事件回调延迟</li><li>紊乱的动画和滚动</li></ul><p>因此,W3C 推出 <a href="https://w3c.github.io/longtasks/">Long Tasks API</a>。长任务(Long task)定义了任何连续不间断的且主 UI 线程繁忙 50 毫秒及以上的时间区间。比如以下常规场景:</p><ul><li>长耗时的事件回调</li><li>代价高昂的回流和其他重绘</li><li>浏览器在超过 50 毫秒的事件循环的相邻循环之间所做的工作</li></ul><blockquote><p>参考 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceLongTaskTiming">Long Tasks API – MDN</a></p></blockquote><p>我们可以使用<code>PerformanceObserver</code>这样简单地获取到长任务:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> observer = <span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="keyword">function</span> (<span class="params">list</span>) {</span><br><span class="line"> <span class="keyword">var</span> perfEntries = list.<span class="title function_">getEntries</span>();</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i < perfEntries.<span class="property">length</span>; i++) {</span><br><span class="line"> <span class="comment">// 分析和上报关键卡顿信息</span></span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line"><span class="comment">// 注册长任务的观察</span></span><br><span class="line">observer.<span class="title function_">observe</span>({ <span class="attr">entryTypes</span>: [<span class="string">"longtask"</span>] });</span><br></pre></td></tr></table></figure><p>相比<code>requestAnimationFrame</code>,使用 Long Tasks API 可避免调用过于频繁的问题,并且<code>performance timeline</code>的任务优先级较低,会尽可能在空闲时进行,可避免影响页面其他任务的执行。但需要注意的是,该 API 还处于实验性阶段,兼容性还有待完善,而我们卡顿常常发生在版本较落后、性能较差的机器上,因此兜底方案也是十分需要的。</p><h3 id="PerformanceObserver-卡顿检测"><a href="#PerformanceObserver-卡顿检测" class="headerlink" title="PerformanceObserver 卡顿检测"></a>PerformanceObserver 卡顿检测</h3><p>前面也提到,卡顿产生于用户操作后网页无法及时响应。根据这个原理,我们可以使用<code>PerformanceObserver</code>监听用户操作,检测是否产生卡顿:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">list</span>) =></span> {</span><br><span class="line"> list.<span class="title function_">getEntries</span>().<span class="title function_">forEach</span>(<span class="function">(<span class="params">entry</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> duration = entry.<span class="property">duration</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> delay = entry.<span class="property">processingStart</span> - entry.<span class="property">startTime</span>;</span><br><span class="line"> <span class="keyword">const</span> eventHandlerTime = entry.<span class="property">processingEnd</span> - entry.<span class="property">processingStart</span>;</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Total duration: <span class="subst">${duration}</span>`</span>);</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Event delay: <span class="subst">${delay}</span>`</span>);</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`Event handler duration: <span class="subst">${eventHandlerTime}</span>`</span>);</span><br><span class="line"> });</span><br><span class="line">}).<span class="title function_">observe</span>({ <span class="attr">type</span>: <span class="string">"event"</span> });</span><br></pre></td></tr></table></figure><p>这种方式的好处是避免频繁在<code>requestAnimationFrame</code>中执行任务,这也是官方鼓励开发者使用的方式,它避免了轮询,且被设计为低优先级任务,甚至可以从缓存中取出过往数据。</p><p>但该方式仅能发现卡顿,至于具体的定位还是得配合埋点和心跳进行会更有效。</p><h2 id="卡顿埋点上报"><a href="#卡顿埋点上报" class="headerlink" title="卡顿埋点上报"></a>卡顿埋点上报</h2><p>不管是哪种卡顿监控方式,我们使用检测卡顿的方案发现了卡顿之后,需要将卡顿进行上报才能及时发现问题。但如果我们仅仅上报了卡顿的发生,是不足以定位和解决问题的。</p><h3 id="卡顿打点"><a href="#卡顿打点" class="headerlink" title="卡顿打点"></a>卡顿打点</h3><p>那么,我们可以通过打点的方式来大概获取卡顿发生的位置。</p><p>举个例子,假设我们一个网页中,关键的点和容易产生长耗时的操作包括:</p><ol><li>加载数据。</li><li>计算。</li><li>渲染。</li><li>批量操作。</li><li>数据提交。</li></ol><p>那么,我们可以在这些操作的地方进行打点。假设我们卡顿工具的能力主要有两个:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">interface <span class="title class_">IJank</span> {</span><br><span class="line"> <span class="attr">_jankLogs</span>: <span class="title class_">Array</span><<span class="title class_">IJankLogInfo</span> & { <span class="attr">logTime</span>: number }>;</span><br><span class="line"> <span class="comment">// 打点</span></span><br><span class="line"> <span class="title function_">log</span>(<span class="attr">jankLogInfo</span>: <span class="title class_">IJankLogInfo</span>): <span class="keyword">void</span>;</span><br><span class="line"> <span class="comment">// 心跳</span></span><br><span class="line"> <span class="title function_">_heartbeat</span>(): <span class="keyword">void</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么,当我们在页面加载的时候分别进行打点,我们的堆栈可能是这样的:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">_jankLogs = [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"加载数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"计算"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"渲染"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"批量操作计算"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"数据提交"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>当卡顿心跳发现卡顿产生时,我们可以拿到堆栈的数据,比如当用户在批量操作之后发生卡顿,假设此时我们拿到堆栈:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">_jankLogs = [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"加载数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"计算"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"渲染"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"批量操作计算"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>这意味着卡顿发生时,最后一次操作是<code>数据层--批量操作计算</code>,则我们可以认为是该操作产生了卡顿。</p><p>我们可以将<code>module</code>/<code>action</code>以及具体的卡顿耗时一起上报,这样就方便我们监控用户的大盘卡顿数据了,也较容易地定位到具体卡顿产生的位置。</p><h3 id="心跳打点"><a href="#心跳打点" class="headerlink" title="心跳打点"></a>心跳打点</h3><p>当然,上述方案如果能达到最优效果,则我们需要在代码中关键的位置进行打点,常见的比如数据加载、计算、事件触发、JavaScript 加载等。</p><p>我们可以将打点方法做成装饰器,自动给<code>class</code>中的方法进行打点。如果埋点数据过少,可能会产生误报,那么我们可以增加心跳的打点:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">IJank</span>.<span class="property">_heartbeat</span> = <span class="function">() =></span> {</span><br><span class="line"> <span class="title class_">IJank</span>.<span class="title function_">log</span>({</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> });</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>当我们心跳产生的时候,会更新堆栈数据。假设发生卡顿的时候,我们拿到这样的堆栈信息:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">_jankLogs = [</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"加载数据"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"计算"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"渲染层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"渲染"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"数据层"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"批量操作计算"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line"> {</span><br><span class="line"> <span class="attr">module</span>: <span class="string">"Jank"</span>,</span><br><span class="line"> <span class="attr">action</span>: <span class="string">"heartbeat"</span>,</span><br><span class="line"> <span class="attr">logTime</span>: xxxxx,</span><br><span class="line"> },</span><br><span class="line">];</span><br></pre></td></tr></table></figure><p>显然,卡顿发生时最后一次打点为<code>Jank--heartbeat</code>,这意味着卡顿并不是产生于<code>数据层---批量操作计算</code>,而是产生于该逻辑后的一个不知名逻辑。在这种情况下,我们可能还需要再在可疑的地方增加打点,再继续观察。</p><h3 id="JavaScript-加载打点"><a href="#JavaScript-加载打点" class="headerlink" title="JavaScript 加载打点"></a>JavaScript 加载打点</h3><p>有一个用于监控一些懒加载的 JavaScript 代码的小技巧,我们可以使用<code>PerformanceObserver</code>获取到 JavaScript 代码资源拉取回来后的时机,然后进行打点:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">performanceObserver = <span class="keyword">new</span> <span class="title class_">PerformanceObserver</span>(<span class="function">(<span class="params">resource</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> entries = resource.<span class="title function_">getEntries</span>();</span><br><span class="line"></span><br><span class="line"> entries.<span class="title function_">forEach</span>(<span class="function">(<span class="params">entry: PerformanceResourceTiming</span>) =></span> {</span><br><span class="line"> <span class="comment">// 获取 JavaScript 资源</span></span><br><span class="line"> <span class="keyword">if</span> (entry.<span class="property">initiatorType</span> !== <span class="string">"script"</span>) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打点</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">log</span>({</span><br><span class="line"> <span class="attr">moduleValue</span>: <span class="string">"compileScript"</span>,</span><br><span class="line"> <span class="attr">actionValue</span>: entry.<span class="property">name</span>,</span><br><span class="line"> });</span><br><span class="line"> });</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="comment">// 监测 resource 资源</span></span><br><span class="line">performanceObserver.<span class="title function_">observe</span>({ <span class="attr">entryTypes</span>: [<span class="string">"resource"</span>] });</span><br></pre></td></tr></table></figure><p>当卡顿产生时,堆栈的最后一个日志如果为<code>compileScript--bundle_xxxx</code>之类的,则可以认为该 JavaScript 资源在加载的时候耗时较久,导致卡顿的产生。</p><p>通过这样的方式,我们可以有效监控用户卡顿的发生,以及卡顿产生较多的逻辑,然后进行相应的问题定位和优化。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>对于计算逻辑较多、页面逻辑复杂的项目来说,卡顿常常是一个较大痛点。</p><p>关于日常性能的数据监控和优化方案之前也有介绍不少,相比一般的性能优化,卡顿往往产生于不合理的逻辑中,比如死循环、过大数据的反复遍历等等,其监控和定位方式也与普通的性能优化不大一致。</p>]]></content>
<summary type="html">
<p>卡顿大概是前端遇到的问题的最棘手的一个,尤其是卡顿产生的时候常常无法进行其他操作,甚至控制台也打开不了。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>从面试角度了解前端基础知识体系</title>
<link href="https://godbasin.github.io/2023/12/25/learn-front-end-develop-from-interview/"/>
<id>https://godbasin.github.io/2023/12/25/learn-front-end-develop-from-interview/</id>
<published>2023-12-25T12:55:22.000Z</published>
<updated>2023-12-25T13:00:22.730Z</updated>
<content type="html"><![CDATA[<p>这两年大裁员过后,带来了一系列的人员变动,常常面临着不受宠的被辞退了,能干的人跑了,剩下的人在努力维护着项目。于是乎老板们才发现人好像又不够了,然后又开始各种招人。机会一直都有,重要的还是得努力提升自己的能力,才能在这场战斗中存活下来。</p><p>前端开发中相对基础的一些内容,主要围绕着 HTML/CSS/Javascript 和浏览器等相关。这些基础知识的掌握是必须的,但有些时候在工作中未必会用到。例如有些项目前后端部署在一起,并不会存在跨域一说,那么可能在开发过程中不会遇到浏览器请求跨域和解决方案相关问题。除了通过不断地学习和在项目中练习,或许从面试的角度来补齐相应的专业知识,可以给我们带来更大的动力。</p><p>本文的内容包括:</p><ul><li>前端专业知识相关面试考察点</li><li>前端项目经验相关面试考察点</li></ul><h2 id="前端专业知识相关面试考察点"><a href="#前端专业知识相关面试考察点" class="headerlink" title="前端专业知识相关面试考察点"></a>前端专业知识相关面试考察点</h2><p>首先我们会针对前端开发相关来介绍需要掌握的一些知识,内容会包括 Javascript、HTML 与 CSS、网络相关、浏览器相关、安全相关、算法和计算机通用知识。</p><p>由于篇幅关系,下面会以关键知识点和问题的方式进行描述,并不会提供具体的答案和详细的内容描述。因此,大家可以针对提到的知识点和问题去进行深入学习和发散,也可以去网上搜一些相关的题目,结合大家的答案去尝试进行理解和解答。</p><h3 id="HTML-与-CSS"><a href="#HTML-与-CSS" class="headerlink" title="HTML 与 CSS"></a>HTML 与 CSS</h3><p>关于 HTML 的内容会较少单独地问,更多是结合浏览器机制等一起考察:</p><ul><li>DOM 操作是否会带来性能问题</li><li>事件冒泡/事件委托</li></ul><p>关于 CSS,也有以下的一些考察点:</p><ul><li>介绍盒子模型</li><li>内联元素与块状元素、<code>display</code></li><li>文档流的理解:<code>static</code>/<code>relative</code>/<code>absolute</code>/<code>fixed</code>等</li><li>元素堆叠:<code>z-index</code>与<code>position</code>的作用关系</li><li>Flex 布局方式的理解和使用</li><li>Grid 布局方式的理解和使用</li><li>BFC 的优点和缺点</li><li>CSS 动画考察:关键帧、<code>animate</code>、<code>transition</code>等</li></ul><p>很多时候,面试官也会通过让候选人编码实现某些样式/元素的方式,来考察候选人对 CSS 的掌握程度,其中布局(居中、对齐等)会比较容易考察到。</p><h3 id="Javascript"><a href="#Javascript" class="headerlink" title="Javascript"></a>Javascript</h3><p>前端最基础的技能包括 Javascript、CSS 和 HTML,尤其是新人比较容易遇到这方面的考察。对于 Javascript 会问到多一些,通常包括:</p><table><thead><tr><th>考察范围</th><th>具体问题</th></tr></thead><tbody><tr><td>对单线程 Javascript 的理解</td><td>单线程来源<br />Web Workers 和 Service Workers 的理解</td></tr><tr><td>异步事件机制</td><td>为什么使用异步事件机制<br />在实际使用中异步事件可能会导致什么问题<br />关于<code>setTimeout</code>、<code>setInterval</code>的时间精确性</td></tr><tr><td>对 EventLoop 的理解</td><td>介绍浏览器的 EventLoop<br />宏任务(MacroTask)和微任务(MicroTask)的区别<br /><code>setTimeout</code>、<code>Promise</code>、<code>async</code>/<code>await</code>在不同浏览器的执行顺序</td></tr><tr><td>Javascript 的原型和继承</td><td>如何理解 Javascript 中的“一切皆对象”<br />如何创建一个对象<br /><code>proto</code>与<code>prototype</code>的区别</td></tr><tr><td>作用域与闭包</td><td>请描述以下代码的执行输出内容(考察作用域)<br />什么场景需要使用闭包<br />闭包的缺陷</td></tr><tr><td><code>this</code>与执行上下文</td><td>简单描述<code>this</code>在不同场景下的指向<br /><code>apply</code>/<code>call</code>/<code>bind</code>的使用<br />箭头函数与普通函数的区别</td></tr><tr><td>ES6+</td><td>对<code>Promise</code>的理解<br />使用<code>async</code>、<code>await</code>的好处<br />浏览器兼容性与 Babel<br /><code>Set</code>和<code>Map</code>数据结构</td></tr></tbody></table><p>对 Javascript 的考察,也可以通过写代码的方式来进行,例如:</p><ul><li>手写代码实现<code>call</code>/<code>apply</code>/<code>bind</code></li><li>手写代码实现<code>Promise</code>、<code>async</code>/<code>await</code><br>-Javascript 中 0.1+0.2 为什么等于 0.30000000000000004,如何通过代码解决这个问题</li></ul><h3 id="网络相关"><a href="#网络相关" class="headerlink" title="网络相关"></a>网络相关</h3><p>网络相关的知识在日常开发中也是挺常用的,相关的问题可以从“一个完整的 HTTP 请求过程”来讲述,包括:</p><ul><li>域名解析(此处涉及 DNS 的寻址过程)</li><li>TCP 的 3 次握手</li><li>建立 TCP 连接后发起 HTTP 请求</li><li>服务器响应 HTTP 请求</li></ul><p>以上的内容都需要尽数掌握,除此以外,关于 HTTP 的还有以下常见内容:</p><ul><li>HTTP 消息的结构,包括 Request 请求、Response 响应</li><li>HTTP 请求方法,使用 PUT、DELETE 等方法时为什么有时候在浏览器会看到两次请求(涉及 CROS 中的简单请求和复杂请求)</li><li>常见的 HTTP 状态码</li><li>浏览器是如何控制缓存的:相应的头信息、状态码</li><li>如何对请求进行抓包和改造</li><li>Websocket 与 HTTP 请求的区别</li><li>HTTPS、HTTP2 与 HTTP 的对比</li><li>网络请求的优化方法</li></ul><h3 id="浏览器相关"><a href="#浏览器相关" class="headerlink" title="浏览器相关"></a>浏览器相关</h3><p>关于浏览器,有很多的机制需要掌握。通常来说,面试官会从一个叫“在浏览器里面输入 URL,按下回车键,会发生什么?”中进行考察,首先会经过上面提到的 HTTP 请求过程,然后还会涉及以下内容:</p><table><thead><tr><th>考察内容</th><th>相关问题</th></tr></thead><tbody><tr><td>浏览器的同源策略</td><td>“同源”指什么<br />那些行为受到同源策略的限制<br />常见的跨域方案有哪些</td></tr><tr><td>浏览器的缓存相关</td><td>Web 缓存通常包括哪些<br />浏览器什么情况下会使用本地缓存<br />强缓存和协商缓存的区别<br />强制<code>ctrl</code>+<code>F5</code>刷新会发生什么<br />session、cookie 以及 storage 的区别</td></tr><tr><td>浏览器加载顺序</td><td>为什么我们通常将 Javascript 放在<code><body></code>的最后面<br />为什么我们将 CSS 放在<code><head></code>里</td></tr><tr><td>浏览器的渲染原理</td><td>HTML/CSS/JS 的解析过程<br />渲染树是怎样生成的<br />重排和重绘是怎样的过程<br />日常开发中要注意哪些渲染性能问题<br /></td></tr><tr><td>虚拟 DOM 机制</td><td>为什么要使用虚拟 DOM<br />为什么要使用 Javascript 对象来描述 DOM 结构<br />简单描述下虚拟 DOM 的实现原理</td></tr><tr><td>浏览器事件</td><td>DOM 事件流包括几个阶段(点击后会发生什么)<br />事件委托是什么</td></tr></tbody></table><h3 id="安全相关"><a href="#安全相关" class="headerlink" title="安全相关"></a>安全相关</h3><p>安全在实际开发中是最重要的,作为前端开发,同样需要掌握:</p><ul><li>前端安全中,需要注意的有哪些问题</li><li>XSS/CSRF 是怎样的攻击过程,要怎么防范</li><li>除了 XSS 和 CSRF,你还了解哪些网络安全相关的问题呢</li><li>SQL 注入、命令行注入是怎样进行的</li><li>DDoS 攻击是什么</li><li>流量劫持包括哪些内容</li></ul><h3 id="算法与数据结构"><a href="#算法与数据结构" class="headerlink" title="算法与数据结构"></a>算法与数据结构</h3><p>很多大公司会考察算法基础,大家其实也可以多上 leetcode 去刷题,这些题目刷多了就有感觉了。前端比较爱考的包括:</p><ul><li>各种排序算法、稳定排序与原地排序、JS 中的 sort 使用的是什么排序</li><li>查找算法(顺序、二分查找)</li><li>递归、分治的理解和应用</li><li>动态规划</li></ul><p>除此之外,常见的数据结构也需要掌握:</p><ul><li>链表与数组</li><li>栈与队列</li><li>二叉树、平衡树、红黑树等</li></ul><p>很多人会觉得,对前端开发来说算法好像并不那么重要,的确日常开发中也几乎用不到。但不管是前端开发也好,还是后台开发、大数据开发等,软件设计很多都是相通的。一些比较著名的前端项目中,也的确会用到一些算法,同样树状数据结构其实也在前端中比较常见。</p><h3 id="计算机通用知识"><a href="#计算机通用知识" class="headerlink" title="计算机通用知识"></a>计算机通用知识</h3><p>同样的,虽然在日常工作中我们接触到的内容比较局限于前端开发,但以下内容作为开发必备基础,也是需要掌握的:</p><ul><li>理解计算机资源,认识进程与线程(单线程、单进程、多线程、多进程)</li><li>了解阻塞与非阻塞、同步与异步任务等</li><li>进程间通信(IPC)常包括哪些方式,进程间同步机制又包括哪些方式</li><li>Socket 与网络进程通信是怎样的关系、Socket 连接过程是怎样的</li><li>简单了解数据库(事务、索引)</li><li>常见的设计模式有哪些、列举实际使用过的一些设计模式</li><li>如何理解面向对象编程、对函数式编程的看法</li></ul><p>基础知识相关的内容真的不少,但是这块其实只要准备足够充分就可以掌握。参加过高考的我们,理解和记忆这么些内容,其实没有想象中那么难的。</p><h2 id="前端项目经验相关面试考察点"><a href="#前端项目经验相关面试考察点" class="headerlink" title="前端项目经验相关面试考察点"></a>前端项目经验相关面试考察点</h2><p>项目经验通常和个人经历关系比较大,前端业务相关的的一些项目经验通常包括管理端、H5 直出、Node.js、可视化,另外还包括参与工具开发的经验,方案选型、架构设计等。</p><p>项目相关的内容,比如性能优化、前端框架之类的,之前我也整理过不少的文章,欢迎大家自己翻阅哦~</p><h3 id="前端框架与工具库"><a href="#前端框架与工具库" class="headerlink" title="前端框架与工具库"></a>前端框架与工具库</h3><p>首先我们来看看前端框架,不管你开发管理端、PC Web、H5,还是现在比较流行的小程序,总会面临要使用某一个框架来开发。因此,以下的问题可能与你有关:</p><ul><li>谈谈你对前端常见的框架(Angular/React/Vue)的理解</li><li>该项目使用 Angular/React/Vue 的原因是</li><li>如果现在你重新决策,你会使用什么框架</li><li>你有了解过这些框架都做了哪些事情,介绍一下是怎么实现的</li><li>Vue 中的双向绑定是怎么实现的?</li><li>介绍下 Angular 中的依赖注入</li><li>讲讲 React 的虚拟 DOM</li><li>如何进行状态管理,Vuex/Redux/Mobx 等工具是怎么做的</li><li>单页应用是什么?路由是如何实现的</li><li>如何进行 SEO 优化</li></ul><p>如果你使用到了小程序,还可能会问到:</p><ul><li>小程序和 H5 有什么不一样,为什么选小程序而不是 H5</li><li>有考虑在小程序里嵌 H5 实现吗,为什么</li><li>为什么小程序的性能要好一些</li><li>小程序开发有用到哪些框架吗、为什么</li></ul><p>而工具库相关的就太多了,一般会这么问:</p><ul><li>有实际使用过哪些第三方库</li><li>这些工具库有什么特性和优缺点</li></ul><p>项目相关的许多问题,其实是我们工作中经常会遇到并需要进行思考的问题。如果平时有养成思考和总结的习惯,那么这些问题很容易就能回答出来。如果平时工作中比较少进行这样的思考,也可以在面试准备的时候多关注下。</p><h3 id="Node-js-与服务端"><a href="#Node-js-与服务端" class="headerlink" title="Node.js 与服务端"></a>Node.js 与服务端</h3><p>Node.js 相关的可能包括:</p><ul><li>为什么要用 Node.js(而不是 PHP/JAVA/GO/C++等)</li><li>Node.js 有哪些特点,单线程的优势和缺点是什么</li><li>Node.js 有哪些定时功能</li><li><code>Process.nextTick</code>和<code>setImmediate</code>的区别</li><li>Node.js 中的异步和同步怎么理解,异步流程如何控制</li><li>简单介绍一下 Node.js 中的核心内置类库(事件,流,文件,网络等)</li><li>express 是如何从一个中间件执行到下一个中间件的</li><li>express、koa、egg 之间的区别</li><li>Rest API 有使用过吗,介绍一下</li></ul><p>以上这些都属于很基础的问题。很多时候,我们会使用 Node.js 去做一些脚本工程或是服务端接入层等工作。如果项目中有使用 Node.js,面试官更多会结合项目相关的进行提问。</p><h3 id="性能优化"><a href="#性能优化" class="headerlink" title="性能优化"></a>性能优化</h3><p>性能优化的其实跟项目比较相关,常见的包括:</p><ul><li>有做过性能优化相关的项目吗,具体的优化过程是怎样的/优化效果是怎样的</li><li>常见的性能优化包括哪些内容</li><li>如何理解项目的性能瓶颈/什么时候我们需要对一个项目进行优化</li><li>图片加载性能有哪些可以优化的地方</li><li>要怎么做好代码分割/降低代码包大小可以有哪些方式</li><li>首屏页面加载很慢,要怎么优化</li><li>Tree Shaking 是怎样一个过程</li><li>页面有没有做什么柔性降级的处理</li></ul><p>很多时候,性能优化也是与项目本身紧紧相关,一般来说会包括首屏耗时优化、页面内容渲染耗时优化、内存优化等,可能涉及代码包大小、下载耗时、首屏直出、存储资源(内存/indexDB)等内容。</p><h3 id="前端工程化"><a href="#前端工程化" class="headerlink" title="前端工程化"></a>前端工程化</h3><p>如今前端工程化的趋势越来越重,通常从脚手架开始:</p><ul><li>为什么我们开发的时候要使用脚手架</li><li>如何理解模块化</li><li>为什么要使用 Webpack,它和 Gulp 的区别是</li><li>讲一下 Webpack 中常用的一些配置、Loader、插件</li><li>Babel 的作用是什么,如何选择合适的 Babel 版本</li><li>Webpack 是怎么将多个文件打包成一个,依赖问题如何解决</li><li>有写过 Webpack 插件吗,Webpack 编译的过程具体是怎样的</li><li>CSS 文件打包过程中,如何避免 CSS 全局污染</li><li>本地开发和代码打包的流程分别是怎样的</li></ul><p>除了脚手架相关,如今自动化、流程化的使用也越来越多了:</p><ul><li>怎么理解持续集成和持续部署</li><li>你们的项目有使用 CI/CD 吗,为什么</li><li>你们的代码有写单元测试/自动化测试吗,为什么</li><li>前端代码支持自动化发布吗,如何做到的</li></ul><p>工程化和自动化是如今前端的一个趋势,由于团队协作越来越多,如何提升团队协作的效率也是一个可具备的技能。</p><h3 id="开发效率提升"><a href="#开发效率提升" class="headerlink" title="开发效率提升"></a>开发效率提升</h3><p>效能提升的意识在工作中很重要,大家都不喜欢低效的加班。通常可能问到的问题包括:</p><ul><li>做了很多的管理端/H5,有考虑过怎么提升开发效率吗</li><li>你的项目里,有没有哪些工作是可以用工具完成的</li><li>项目中有进行组件和公共库的封装吗</li><li>如何管理这些公共组件/工具的兼容问题</li><li>日常工作中,如何提升自己的工作效率</li></ul><h3 id="监控、灰度与发布"><a href="#监控、灰度与发布" class="headerlink" title="监控、灰度与发布"></a>监控、灰度与发布</h3><p>发布和监控这部分,可能较大的业务才会有,涉及的问题可以有:</p><ul><li>日常开发过程中,怎么保证页面质量</li><li>版本发布有进行灰度吗?灰度的过程是怎样的</li><li>版本发布过程中,如何及时地发现问题</li><li>发生异常,要怎么快速地定位到具体位置</li><li>如何观察线上代码的运行质量</li></ul><p>对于大型项目来说,灰度发布几乎是开发必备,而监控和问题定位也需要各式各样的工具来辅助优化。</p><h3 id="多人协作"><a href="#多人协作" class="headerlink" title="多人协作"></a>多人协作</h3><p>一些较大的项目,通常由多个开发合作完成。而多人协作的经验也很有帮助:</p><ul><li>多人开发过程中,代码冲突如何解决</li><li>项目中有使用 Git 吗?介绍一下 Git flow 流程</li><li>如果项目频繁交接,如果提升开发效率</li><li>有遇到代码习惯差异的问题吗,如何解决</li><li>有哪些常用的代码校验的工具</li><li>怎么强制进行 Code Review</li></ul><p>看到这么多内容不要慌,一般来说面试官只会根据你的工作经历来询问对应的问题,所以如果你并没有完全掌握某一块的内容,请不要写在简历上,你永远也不知道面试官会延伸到哪。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>专业知识也好,项目经验也好,充分的准备可以留给面试官不错的印象。但这些都未必能完全体现日常工作和思考的一些能力,面试官通常会通过编程题、逻辑思维开放题等其他角度来。</p><p>同时,对于程序员来说,自学是很关键的一个能力,面试官也可能会通过职业规划、学习习惯等角度,了解候选人对技术的热情、是否好学、抗压能力、解决问题能力等,来判断候选人是否符合团队要求、是否适合团队氛围。</p><p>而从面试的角度来介绍这些内容,除了可以有方向地进行知识储备,更多的是希望大家能结合自身的实际情况反思自己是否还有可以改善的地方,因为面试过程中考察的点通常便是实际工作中会遇到的问题。</p><p>最后,圣诞夜,祝各位顺颂冬安~</p>]]></content>
<summary type="html">
<p>这两年大裁员过后,带来了一系列的人员变动,常常面临着不受宠的被辞退了,能干的人跑了,剩下的人在努力维护着项目。于是乎老板们才发现人好像又不够了,然后又开始各种招人。机会一直都有,重要的还是得努力提升自己的能力,才能在这场战斗中存活下来。</p>
<p>前端开发中相对基础的一
</summary>
<category term="前端技能提升" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD%E6%8F%90%E5%8D%87/"/>
<category term="前端技能" scheme="https://godbasin.github.io/tags/%E5%89%8D%E7%AB%AF%E6%8A%80%E8%83%BD/"/>
</entry>
<entry>
<title>复杂渲染引擎架构与设计--8.元素与事件</title>
<link href="https://godbasin.github.io/2023/11/25/render-engine-element-and-event/"/>
<id>https://godbasin.github.io/2023/11/25/render-engine-element-and-event/</id>
<published>2023-11-25T13:42:02.000Z</published>
<updated>2023-11-25T13:42:50.856Z</updated>
<content type="html"><![CDATA[<p>前面提到了渲染引擎的几种渲染方式,如果我们要在与用户交互的过程中识别到具体的元素,又该如何处理呢?</p><span id="more"></span><h2 id="Canvas-元素选择的难题"><a href="#Canvas-元素选择的难题" class="headerlink" title="Canvas 元素选择的难题"></a>Canvas 元素选择的难题</h2><p>在<a href="https://godbasin.github.io/2023/07/19/render-engine-bottom-render-architecture/">《复杂渲染引擎架构与设计–3.底层渲染适配》</a>一文中,我们介绍了不同的渲染方式,包括 Canvas 渲染、DOM 渲染、SVG 渲染甚至是 WebGL 渲染等。对于不同的渲染方式,要实现元素选取的代价十分不一样。</p><p>对于 DOM/SVG 渲染,我们可以直接使用浏览器提供的元素选择能力。在这样的场景下,不管是父子元素的管理、事件冒泡和捕获等都比较容易实现。因此,我们今天主要讨论 Canvas 渲染要如何实现元素选择。</p><p>对于 Canvas 渲染里进行元素选取,我们常见有几种方式:</p><ol><li>几何检测法。</li><li>像素检测法。</li><li>Canvas + DOM 绘制交互。</li></ol><h3 id="几何检测法"><a href="#几何检测法" class="headerlink" title="几何检测法"></a>几何检测法</h3><p>几何检测法是许多游戏引擎或者说物理引擎的解决方案,我们常常又称为碰撞检测法。</p><p>在元素选取的场景下,我们只需要判断用户触发事件的位置,是否落在某个元素几何里。因此,我们面临的问题是:确定某个点是否位于给定的多边形内。</p><p>一般来说,某个点是否在某个多边形内,常见的便是交叉数法(也称射线判断法):从所讨论的点 P 向任何方向射出一条射线(半线),判断该射线与元素几何相交的线段数奇偶情况。在该方法中,我们需要确保射线不会直接射到多边形的任何顶点,这个是比较难做到的。因此,也有不少改良的方案,具体可以参考<a href="https://web.cs.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html">《When is a Point Inside a Polygon?》</a>。</p><p>除了交叉数法,还有环绕数法,这里不进行详细解释了,具体可以参考<a href="https://zhuanlan.zhihu.com/p/436494294">《Canvas 中判断点是否在图形上》</a>。</p><p>几何检测法在渲染引擎中使用,优势在于内存消耗小。但它也存在一些问题:</p><ol><li>需要维护一个图形检测算法库。</li><li>对于复杂的曲线图形计算量很大。</li></ol><p>除此之外,如果元素存在堆叠的情况,则可能需要遍历地进行检测判断;如果存在的元素数量特别庞大,则意味着这样的遍历性能可能会受到影响。</p><h3 id="像素检测法"><a href="#像素检测法" class="headerlink" title="像素检测法"></a>像素检测法</h3><p>像素检测法又称色值法,简单来说就是给每个绘制的图案按照堆叠顺序绘制一个带随机背景色的一样的图案,当某个点落在 Canvas 上时,则可以通过所在的像素颜色,找到对应的几何元素。</p><p>这个方法看似简单,实际上我们需要使用两个 Canvas 来实现:</p><ul><li>一个用于真实渲染使用,绘制最终用户可见的内容</li><li>另一个用于交互使用,带背景色的图形将会绘制在这个 Canvas 上</li></ul><p>当用户进行交互时,通过 Canvas 位置找出第二个 Canvas 的颜色,然后根据色值去获取到对照的图形。这便要求我们每个图形在绘制前,都需要生成一个元素与随机色值的映射表,通过该表才能获取到最终的元素。</p><p>像素检测法的实现很简单,这也是它的优势,但是同样会存在一些问题:</p><ul><li>维护两个 Canvas 需要一定的成本</li><li>如果绘制频繁,或者绘制内容重新渲染频繁,则该方法也会存在性能问题</li></ul><p>如果考虑到像在线表格这样的产品,由于还需要滚动和重绘,像素检测法的性能或许不会很好。而表格本身就是天然四方形的布局,因此更适合使用几何检测法,而在使用几何检测法的时候,我们甚至只需要判断某个点是否落在某个矩形内,几乎涉及不到较复杂的算法。</p><h3 id="Canvas-DOM-交互"><a href="#Canvas-DOM-交互" class="headerlink" title="Canvas + DOM 交互"></a>Canvas + DOM 交互</h3><p>对于一些复杂的交互场景,我们可以适当地添加 DOM 元素来降低维护成本。</p><p>比如,在线表格的交互中,很多时候我们都需要先选中一些格子,然后再进行操作。那么,我们可以先使用简单的几何检测法来获取到对应单元格位置,然后生成一个对应的 DOM 元素覆盖在对应的 Canvas 上,之后所有的交互都由这个 DOM 元素来完成。</p><p>显然,像输入编辑这种功能,是无法完全使用 Canvas 实现的,或者说是成本巨大,因此我们可以直接使用一个 DOM 的编辑框放在 Canvas 上面,等用户完成编辑操作,再把内容同步到 Canvas 上即可。</p><p>这是一种比较简单又取巧的解决方案,但同样需要考虑一些问题:</p><ul><li>页面滚动的时候,DOM 元素是否需要跟随滚动</li><li>页面发生变化的时候,DOM 元素是否需要刷新</li></ul><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文主要介绍了 Canvas 里实现元素获取和事件处理的几种解决方案。</p><p>其实我们并不是所有时候都需要硬啃复杂的算法或是解决方案,换一下思路,其实你会发现有无数的方向可以尝试。虽然很多时候,我们常说要参考业界常见成熟的方案,但这并不意味着我们就一定要照抄。</p><p>适合自己的才是最好的,不管它是大众的方案,还是某种特殊场景下的解决方案。</p>]]></content>
<summary type="html">
<p>前面提到了渲染引擎的几种渲染方式,如果我们要在与用户交互的过程中识别到具体的元素,又该如何处理呢?</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>复杂渲染引擎架构与设计--7.离屏渲染</title>
<link href="https://godbasin.github.io/2023/10/13/render-engine-offscreen-render/"/>
<id>https://godbasin.github.io/2023/10/13/render-engine-offscreen-render/</id>
<published>2023-10-13T02:06:11.000Z</published>
<updated>2023-10-13T02:13:01.028Z</updated>
<content type="html"><![CDATA[<p>前面我们介绍了增量渲染的解决方案,其中有提到复用 Canvas 进行性能优化的解决方案。</p><p>本文我们将结合 Canvas 的能力提出进一步的优化方案:离屏渲染。</p><span id="more"></span><p>上一篇<a href="https://godbasin.github.io/2023/10/11/render-engine-diff-render/">《6.增量渲染》</a>提到页面滚动时 Canvas 复用的场景,这种场景下我们还可以考虑两种方式:</p><ol><li>使用 Canvas 上一帧画像,直接转换成图片复用到下一帧的绘制中。</li><li>维护两个 Canvas,滚动时使用两个 Canvas 交替绘制。</li></ol><p>第二种方式中,当前渲染的 Canvas 与隐藏的缓存 Canvas 交替渲染,由于会使用一个屏幕外(非可视)的 Canvas 进行提前绘制,我们也可以称之为离屏渲染。</p><h2 id="离屏渲染"><a href="#离屏渲染" class="headerlink" title="离屏渲染"></a>离屏渲染</h2><p>离屏渲染可以提前将更大范围的内容绘制好,在滚动时可直接取对应的区域进行截取和绘制。</p><p>当然,两个 Canvas 的维护和绘制成本会比一个 Canvas 要更高,同时如果需要提前绘制更大区域的单元格范围,那么必然会面临一个问题:需要更多的计算和渲染消耗。</p><p>我们可以考虑另外一个优化方案:使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas">OffscreenCanvas</a> 实现真正的离屏。</p><h3 id="OffscreenCanvas-API-能力"><a href="#OffscreenCanvas-API-能力" class="headerlink" title="OffscreenCanvas API 能力"></a>OffscreenCanvas API 能力</h3><p>OffscreenCanvas 是一个实验中的新特性,主要用于提升 Canvas 2D/3D 绘图应用和 H5 游戏的渲染性能和使用体验。OffscreenCanvas 目前主要用于两种不同的使用场景:</p><ol><li>同步显示 OffscreenCanvas 中的帧。在 Worker 线程创建一个 OffscreenCanvas 做后台渲染,然后再把渲染好的缓冲区 Transfer 回主线程显示。</li><li>异步显示 OffscreenCanvas 中的帧。主线程从当前 DOM 树中的 Canvas 元素产生一个 OffscreenCanvas,再把这个 OffscreenCanvas 发送给 Worker 线程进行渲染,渲染的结果直接 Commit 到浏览器的 Display Compositor 输出到当前窗口,相当于在 Worker 线程直接更新 Canvas 元素的内容。</li></ol><p>整体的离屏方案依赖 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/OffscreenCanvas">OffscreenCanvas</a> 提供的能力,关于此能力现有的技术方案和文档较少,可参考:</p><ul><li><a href="https://zhuanlan.zhihu.com/p/34698375">OffscreenCanvas - 概念说明及使用解析</a></li><li><a href="https://developers.google.com/web/updates/2018/08/offscreen-canvas"></a></li></ul><p>在我们的架构设计下,更适合使用第一种方案,即同步显示 OffscreenCanvas 中的帧。这样设计的优势在于:当主线程繁忙时,依然可以通过 OffscreenCanvas 在 worker 中更新画布内容,避免给用户造成页面卡顿的体验。</p><p>除此之外,还可以进一步考虑在兼容性支持的情况下,通过将局部计算运行在 worker 中,减少渲染引擎的计算耗时,提升渲染引擎的渲染性能。</p><p>当然,如果要实现在 Worker 中进行提前渲染,则需要考虑如何将渲染引擎提供给 Worker,以及将数据及时同步到 Worker 的问题。</p><h3 id="渲染引擎与-Worker"><a href="#渲染引擎与-Worker" class="headerlink" title="渲染引擎与 Worker"></a>渲染引擎与 Worker</h3><p>如果想完全发挥到 OffscreenCanvas 的作用,要支持真正意义上的离屏渲染,而不是在主线程使用一个隐藏的 Canvas 交替绘制,需要考虑:</p><ol><li>渲染引擎放置在 worker 中是否合适?</li></ol><p>由于渲染引擎本身是需要实时响应用户的操作的,因此大部分的内容更新是需要同步计算、并更新到 Canvas 中的。如果提取到 worker 中进行,需要考虑是否由于线程通信的原因导致响应速度的降低,反而影响用户体验。</p><ol start="2"><li>哪些计算可以放到 worker 中异步运行?</li></ol><p>方向一:每次有数据更新,渲染引擎都会全量更新和计算,可以考虑将非可视区域范围的部分(即可视范围往后的部分)放置到 worker 和离屏 Canvas 中进行计算</p><p>方向二:前面提到,渲染引擎的渲染分为两部分:</p><ul><li>表格主体内容渲染(单元格内容、边框线、背景色等)</li><li>业务通过插件添加额外的内容渲染(图标、背景高亮等)</li></ul><p>对于插件部分内容,可以考虑将其放到 worker 中计算并更新。但局部内容异步渲染,可能需要考虑对当前 Canvas 进行改造,进行分层渲染,即可按照堆叠顺序进行 Canvas 拆分,结合每块内容的更新频率,仅更新某种类型的绘制内容。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>对于项目中是否适合使用该离屏方案,需要结合项目自身的架构设计、改造成本和兼容性问题等情况,考虑好上述问题,才能决定。即使是在 Worker 中不阻塞主线程,依然需要考虑计算量过大可能会导致渲染延迟等问题。</p><p>它会带来不小的改造成本,但收益是否可观还需要观察,你也可以先编写一个 demo 来确认效果,再尝试在项目中接入使用。</p>]]></content>
<summary type="html">
<p>前面我们介绍了增量渲染的解决方案,其中有提到复用 Canvas 进行性能优化的解决方案。</p>
<p>本文我们将结合 Canvas 的能力提出进一步的优化方案:离屏渲染。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>复杂渲染引擎架构与设计--6.增量渲染</title>
<link href="https://godbasin.github.io/2023/10/11/render-engine-diff-render/"/>
<id>https://godbasin.github.io/2023/10/11/render-engine-diff-render/</id>
<published>2023-10-11T13:31:02.000Z</published>
<updated>2023-10-13T02:07:56.050Z</updated>
<content type="html"><![CDATA[<p>对于渲染引擎来说,如果每次都进行完整内容的计算和绘制,在低端机器或是负责页面的时候可能会出现卡顿。</p><p>因此,我们可以考虑设计一套增量渲染的能力,来实现改多少、重绘多少,减少每次渲染的耗时,提升用户的体验。</p><span id="more"></span><h2 id="增量渲染设计"><a href="#增量渲染设计" class="headerlink" title="增量渲染设计"></a>增量渲染设计</h2><p>所谓增量渲染,或许你已经从 React/Vue 等框架中有所耳闻,即更新仅需要更新的部分内容,而不是每次都重新计算和渲染。</p><h3 id="React-增量渲染"><a href="#React-增量渲染" class="headerlink" title="React 增量渲染"></a>React 增量渲染</h3><p>React 里结合了虚拟 DOM 以及 Fiber 引擎来实现完整的 Diff 计算和渲染调度,这些我之前在其他文章也有说过。在 React 里,状态的更新机制主要由两个步骤组成:</p><ol><li>找出变化的组件,每当有更新发生时,协调器会做如下工作:</li></ol><ul><li>调用组件 render 方法将 JSX 转化为虚拟 DOM</li><li>进行虚拟 DOM Diff 并找出变化的虚拟 DO</li></ul><ol start="2"><li>通知渲染器。渲染器接到协调器通知,将变化的组件渲染到页面上。</li></ol><p>我们的渲染引擎道理也是十分相似的,即找出最小变化范围进行计算和更新。同样的,我们还是继续以在线表格为例子,基于我们现有的引擎设计上实现增量渲染。</p><h3 id="收集增量"><a href="#收集增量" class="headerlink" title="收集增量"></a>收集增量</h3><p>关于渲染引擎的收集和渲染过程,已经在前面<a href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/">《1.收集与渲染》</a>文章中介绍过。</p><p>基于该架构设计,我们知道一次渲染分成两个过程:</p><ol><li>收集渲染数据。</li><li>绘制收集后的渲染数据。</li></ol><p>而前面在<a href="https://godbasin.github.io/2023/06/15/render-engine-plugin-design/">《2.插件的实现》</a>中也提到,渲染引擎整体的架构如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-plugin-design-1.jpg" alt=""></p><p>在该架构图中,渲染引擎支持提供绘制特定范围的能力。而要实现这样的能力,我们需要做到:</p><ol><li>支持特定范围的渲染数据收集。</li><li>支持特定范围的 Canvas 画布重绘。</li></ol><p>由于在线表格这样的产品都是以单元格为基础,因此我们的收集器和渲染器都同样可以以单元格为最小单位,提供以下的能力:</p><ol><li>根据格子位置更新、清理、新增收集的渲染数据。</li><li>根据格子位置进行画布的擦除和重新绘制。</li></ol><p>实际上,一次渲染的耗时大头更多会出现在收集过程,因为收集过程中常常会进行较复杂的计算,亦或是针对一个个格子的数据收集会导致不停地遍历各个格子范围和访问特定对象获取数据。</p><p>所以更重要的增量能力在于收集过程的增量。</p><h2 id="在线表格增量渲染"><a href="#在线表格增量渲染" class="headerlink" title="在线表格增量渲染"></a>在线表格增量渲染</h2><p>对于在线表格的场景,我们可以考虑两种增量渲染的情况:</p><ol><li>局部修改,比如用户修改了某个范围的格子内容和样式。</li><li>页面滚动,用户滚动过程中,有部分单元格范围不变。</li></ol><p>我们分别来看看。</p><h3 id="局部修改"><a href="#局部修改" class="headerlink" title="局部修改"></a>局部修改</h3><p>局部修改比较简单,前面我们已经提到说收集过程和渲染过程都支持按指定范围进行增量渲染,因此局部修改的时候直接走特定范围的绘制即可。比如用户修改了 A1 这个格子:</p><ol><li>清除 A1 单元格的收集数据。</li><li>重新收集 A1 单元格的收集数据。</li><li>重新渲染 A1 单元格的内容。</li></ol><h3 id="页面滚动"><a href="#页面滚动" class="headerlink" title="页面滚动"></a>页面滚动</h3><p>页面滚动与纯某个特定范围的修改不大一样,因为页面滚动过程中,所有单元格的位置都会发生改变。</p><p>一般来说,在滚动过程我们会产生局部可复用的单元格绘制结果,如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-diff-render-1.jpg" alt=""></p><p>对于这样的情况,我们可以有两种解决方案:</p><ol><li>复用局部 Canvas 绘制结果。</li><li>复用局部收集的渲染数据结构。</li></ol><p>方案 1 直接复用局部 Canvas 的方案比较简单,不少在线表格像谷歌表格、飞书文档等都是用的该方案,该方案同样存在一些问题:</p><ul><li>由于 Canvas 绘制在非整数像素下会存在不准确的问题,因此在有缩放比例下增量渲染会出现多余的横线、白线等问题</li><li>由于该过程会将原有 Canvas 的内容先转成图片,再往新的内容区域贴进去,会导致 Canvas 透明度丢失,无法支持 Canvas 的透明设置</li></ul><p>方案 2 复用局部收集的渲染数据结构,可以优化上述问题,但整体的性能会比复用 Canvas 稍微差一些,毕竟复用 Canvas 直接节省了复用范围的收集和渲染耗时,而复用收集结果则仅节省了复用范围的收集,绘制过程还是会全量绘制。</p><p>对于复用收集结果的方案,还需要考虑页面出现滚动导致的绘制位置差异,即使是同一个单元格,其在画布上的位置也发生了改变,这样的变化需要考虑进去。</p><p>因此,收集器的数据结构需要和单元格紧密相关,而不是基于 Canvas 的整体偏移。如何设计出性能较好又易于理解的数据结构,这也是一项不小的挑战,决定了我们增量渲染的优化效果能到哪里。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>由于渲染引擎和用户视觉、交互紧密相关,因此常常是性能优化的大头。结合产品特点和架构设计做具体的分析和优化,这才是我们在实际工作中常常面临的挑战。</p><p>前面介绍过的分片优化也好,这里的增量渲染也好,其实大多数都能在业界找到类似的思路来做参考。不要把思路局限在相同产品、相同场景下的解决方案,即使是看似毫不相干的优化场景,你也能拓展思维看看对自己遇到的难题是否能有所启发。</p>]]></content>
<summary type="html">
<p>对于渲染引擎来说,如果每次都进行完整内容的计算和绘制,在低端机器或是负责页面的时候可能会出现卡顿。</p>
<p>因此,我们可以考虑设计一套增量渲染的能力,来实现改多少、重绘多少,减少每次渲染的耗时,提升用户的体验。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>复杂渲染引擎架构与设计--5.分片计算</title>
<link href="https://godbasin.github.io/2023/09/16/render-engine-calculate-split/"/>
<id>https://godbasin.github.io/2023/09/16/render-engine-calculate-split/</id>
<published>2023-09-16T13:29:10.000Z</published>
<updated>2023-09-16T13:30:23.750Z</updated>
<content type="html"><![CDATA[<p>前面<a href="https://godbasin.github.io/2023/08/17/render-engine-calculate/">《渲染计算》</a>一文中,我们提到了对于长耗时的渲染计算的优化方案,其中便包括了将大的计算任务拆分为小任务的方式。</p><p>本文我们以在线表格为例子,详细介绍下如何对长耗时的计算进行任务拆解。</p><span id="more"></span><h2 id="渲染引擎计算任务分片优化"><a href="#渲染引擎计算任务分片优化" class="headerlink" title="渲染引擎计算任务分片优化"></a>渲染引擎计算任务分片优化</h2><p>在表格中,当数据发生更新变化时(可能是用户本身的操作,也可能是协作者),渲染引擎接收到数据变更,然后进行计算和更新渲染。流程如下:</p><ol><li>渲染引擎监听表格数据的数据变化。</li><li>数据变化发生时,渲染引擎筛选出相关的,并对数据进行计算,转换为渲染引擎需要的数据。</li><li>根据计算后的数据,将表格内容绘制到 Canvas 画布中(收集 + 渲染)。</li></ol><p>上述的步骤 2 中,渲染引擎计算均为同步计算,因此随着计算范围的增加,所需耗时会随之增长。</p><p>在这样的基础上,我们提出了将渲染引擎计算任务进行分片的方案。该方案主要优化的位置位于渲染引擎的计算过程,可减少在大范围、大表下的操作(如列宽调整、大范围选区的样式设置等)卡顿。</p><h3 id="核心优化方案"><a href="#核心优化方案" class="headerlink" title="核心优化方案"></a>核心优化方案</h3><p>本次渲染引擎计算任务分片的方案核心点在于:<strong>只进行可视区域的渲染计算,非可视区域的部分做异步计算。</strong></p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-splitter-calculate-4.jpg" alt=""></p><p>如图,当一次数据变更发生时,渲染引擎会根据变更范围,将计算任务拆成两部分:可视区域和非可视区域的计算任务。</p><p>整个计算异步分片方案中,有以下几个核心设计点:</p><ol><li>对于当前可视区域的部分,会进行同步的计算和渲染。</li><li>对于非可视区域的部分,会进行异步分片(约 50ms 为一次计算分片)。</li><li>异步计算时,会优先计算当前可视区域附近范围的部分区域。</li><li>异步计算过程中,如涉及当前可视区域的变动,会触发重新渲染;对于非可视区域部分的计算,不会触发重新渲染。</li><li>对多次的操作,未计算部分的区域会进行合并计算,可减少整体的计算量。</li></ol><p>我们来看一下其中的待计算区域管理和异步任务管理的部分设计。</p><h2 id="待计算区域管理"><a href="#待计算区域管理" class="headerlink" title="待计算区域管理"></a>待计算区域管理</h2><p>首先,我们提供了一个区域管理的能力,里面存储了未计算完成的区域。区域管理的能力需要满足:</p><ol><li>区域生成:生成一个区域,包括行/列范围、计算任务的类型(分行/覆盖格/边框线等);</li><li>区域合并:对两个区域进行合并,并更新区域范围;</li><li>区域获取:根据提供的区域范围,获取该区域内的待计算任务;</li><li>区域更新:行/列变化快速更新区域范围。</li></ol><p>由于渲染引擎计算的特殊性(大多数计算为按行计算),区域考虑以行为首要维度、列为次要维度的方式来管理,因此区域的设计大概为:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">IAreaRange</span> = {</span><br><span class="line"> <span class="comment">// 开始行 index</span></span><br><span class="line"> <span class="attr">rowStart</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="comment">// 结束行 index</span></span><br><span class="line"> <span class="attr">rowEnd</span>: <span class="built_in">number</span>;</span><br><span class="line"> <span class="comment">// 列范围 [开始列 index, 结束列 index]</span></span><br><span class="line"> <span class="attr">colRanges</span>: [<span class="built_in">number</span>, <span class="built_in">number</span>][];</span><br><span class="line"> <span class="comment">// 行范围的计算类型</span></span><br><span class="line"> <span class="attr">calculateTypes</span>: <span class="title class_">CalculateType</span>[];</span><br><span class="line">};</span><br></pre></td></tr></table></figure><h3 id="区域合并"><a href="#区域合并" class="headerlink" title="区域合并"></a>区域合并</h3><p>对于两个区域的合并,需要考虑相交和不相交的情况。不相交时不需要做合并,而对于相交的情况,还需要考虑合并的方式,主要考虑单边相交和包含关系的合并:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-splitter-calculate-5.jpg" alt=""></p><p>根据计算类型和列范围,且考虑边界场景下,两个区域合并后可能会转换为 1/2/3 个区域。</p><h3 id="区域更新"><a href="#区域更新" class="headerlink" title="区域更新"></a>区域更新</h3><p>由于区域本身依赖了行列位置,因此当行列发生改变时,比如插入/删除/隐藏/移动(即插入+删除)等场景,我们需要及时更新区域。以行变化为例:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-splitter-calculate-6.jpg" alt=""></p><p>同样需要考虑边界场景,比如删除区域覆盖了整个(或局部)区域等。</p><h2 id="异步任务管理"><a href="#异步任务管理" class="headerlink" title="异步任务管理"></a>异步任务管理</h2><p>异步任务管理的设计采用了十分简洁的方式(一个<code>setTimeout</code>任务)来实现:</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">AsyncCalculateManager</span> {</span><br><span class="line"> <span class="comment">// 每次执行任务的耗时</span></span><br><span class="line"> <span class="keyword">static</span> timeForEveryTask = <span class="number">50</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 跑下一次任务</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">runNext</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">timer</span>) <span class="built_in">clearTimeout</span>(<span class="variable language_">this</span>.<span class="property">timer</span>);</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">timer</span> = <span class="built_in">setTimeout</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// 一个任务跑 50 ms</span></span><br><span class="line"> <span class="keyword">const</span> calculateRange = <span class="variable language_">this</span>.<span class="property">calculateRunner</span>.<span class="title function_">calculateNextTask</span>(</span><br><span class="line"> <span class="title class_">AsyncCalculateManager</span>.<span class="property">timeForEveryTask</span></span><br><span class="line"> );</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 处理完之后,剩余任务做异步</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">runNext</span>();</span><br><span class="line"> }, <span class="number">10</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述代码可以看到,每个任务执行耗时满 50ms 后,会结束当前任务,并设置下一个异步任务。通过这样的方式,我们将每次计算任务控制在 50ms 左右,避免计算过久而导致的卡顿问题。</p><h3 id="异步任务设计"><a href="#异步任务设计" class="headerlink" title="异步任务设计"></a>异步任务设计</h3><p>对于异步任务,每次执行的时候,都需要:</p><ol><li>根据当前可视区域,优先选出可视区域附近的任务来进行计算。</li><li>计算完成后,清理和更新待计算区域范围。</li></ol><p>对于 1,可视区域内如果存在未计算的任务,会以符合阅读习惯的从上到下进行计算;如果可视区域内均已计算完毕,则会以可视区域为中心,向两边寻找未计算任务,并进行计算。如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-splitter-calculate-7.jpg" alt=""></p><p>异步任务计算时,还需要考虑计算的范围是否涉及可视区域,如果在可视区域内有计算任务,则需要进行渲染;如果计算任务处于非可视区域,则可以避免进行不必要的渲染。</p><h3 id="异步计算的问题"><a href="#异步计算的问题" class="headerlink" title="异步计算的问题"></a>异步计算的问题</h3><p>将原本同步计算的任务拆成多个异步的计算任务,会面临一些问题包括:</p><ul><li>各个计算任务之间的顺序,比如边框线依赖覆盖格、行高依赖分行等;</li><li>可视区域的锁定(避免跳动),由于行高会在滚动过程中进行异步计算和更新,可能会存在可视区域内容跳动(原本可见变为不可见)的问题;</li><li>按坐标滚动(位置记忆、会议跟随等功能),考虑到行高会在滚动过程发生变化,按坐标滚动的相关功能会受到计算不准确等影响;</li><li>边滚动边计算,如果更新不及时,可能导致一些组件的闪动和错位的问题;</li></ul><p>解决方案大概是:确保每次计算后,行列宽高、可视区域、画布偏移等位置数据的一致性。要做到所有数据的一致性,需要对各个节点的流程做整体梳理,这里就不详细展开了。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文以在线表格的分片计算为例,详细介绍了如何将大的计算任务拆分成小任务,减少了渲染等待的计算耗时。</p><p>我们常常会将产品和技术分离,认为技术需求占用了产品需求的人力,或是认为产品需求导致技术频繁变更。实际上,技术依附于产品而得以实现,产品亦是需要技术作为支撑。</p><p>每一个项目都需要不断地打磨,我们在产品快速向前迭代的同时,也需要实时关注项目本身的基础能力是否能满足产品未来的规划和方向。</p>]]></content>
<summary type="html">
<p>前面<a href="https://godbasin.github.io/2023/08/17/render-engine-calculate/">《渲染计算》</a>一文中,我们提到了对于长耗时的渲染计算的优化方案,其中便包括了将大的计算任务拆分为小任务的方式。</p>
<p>本文我们以在线表格为例子,详细介绍下如何对长耗时的计算进行任务拆解。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>复杂渲染引擎架构与设计--4.渲染计算</title>
<link href="https://godbasin.github.io/2023/08/17/render-engine-calculate/"/>
<id>https://godbasin.github.io/2023/08/17/render-engine-calculate/</id>
<published>2023-08-17T07:22:12.000Z</published>
<updated>2023-08-17T07:24:38.539Z</updated>
<content type="html"><![CDATA[<p>前面<a href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/">《收集与渲染》</a>一文中,我们简单提到说在一些复杂场景下,从服务端获取的数据还需要进行计算,比如依赖 Web 浏览器的计算,亦或是游戏引擎中的碰撞检测。</p><p>本文我们详细针对复杂计算的场景来考虑渲染引擎的优化。</p><span id="more"></span><h2 id="渲染引擎完整的数据流向"><a href="#渲染引擎完整的数据流向" class="headerlink" title="渲染引擎完整的数据流向"></a>渲染引擎完整的数据流向</h2><p>对于需要进行较复杂计算的渲染场景,结合收集和渲染的架构设计,我们完整的渲染流程大概应该是这样的:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-calculate-1.jpg" alt=""></p><p>可见,完整的渲染流程里,计算的复杂程度会直接影响渲染是否及时,最终影响到用户的交互体验。在这里,我们还是以在线表格为例子,详细介绍下为什么有如此大的计算任务。</p><h3 id="渲染引擎为什么需要计算"><a href="#渲染引擎为什么需要计算" class="headerlink" title="渲染引擎为什么需要计算"></a>渲染引擎为什么需要计算</h3><p>在表格中,画布绘制所需的数据,并不能完全从数据层中获取得到。对于以下一些情况,需要经过渲染引擎的计算处理才能正确绘制到画布上,包括:</p><h4 id="1-分行-换行计算(计算范围:格子)"><a href="#1-分行-换行计算(计算范围:格子)" class="headerlink" title="1. 分行/换行计算(计算范围:格子)"></a>1. 分行/换行计算(计算范围:格子)</h4><p>如下图,当单元格设置了自动换行,当格子内容超过一行会被自动换到下一行。由于内容宽度的测量依赖浏览器环境,因此也是需要在渲染引擎进行计算的:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-splitter-calculate-2.jpg" alt=""></p><h4 id="2-行高计算(计算范围:整行)"><a href="#2-行高计算(计算范围:整行)" class="headerlink" title="2. 行高计算(计算范围:整行)"></a>2. 行高计算(计算范围:整行)</h4><p>当某个行没有设置固定的行高时,该行内容的高度可能会存在被自动换行的单元格撑高的情况,因此真实渲染的行高也需要根据分行/换行结果进行计算。</p><h4 id="3-覆盖格-隐藏格计算(计算范围:整行)"><a href="#3-覆盖格-隐藏格计算(计算范围:整行)" class="headerlink" title="3. 覆盖格/隐藏格计算(计算范围:整行)"></a>3. 覆盖格/隐藏格计算(计算范围:整行)</h4><p>如下图,在没有设置自动换行的情况下,当单元格内容超出当前格子,会根据对齐的方向、该方向上的格子是否有内容,向对应的方向拓展内容,呈现向左右两边覆盖的情况:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-splitter-calculate-1.jpg" alt=""></p><h4 id="4-边框线计算(计算范围:整行)"><a href="#4-边框线计算(计算范围:整行)" class="headerlink" title="4. 边框线计算(计算范围:整行)"></a>4. 边框线计算(计算范围:整行)</h4><p>受覆盖格影响,覆盖格和隐藏格(即被覆盖的格子)间的边框线会被超出的内容遮挡,因此对应的边框线也会受影响。</p><p>以调整列宽为例子,该操作涉及的计算包括:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-splitter-calculate-3.jpg" alt=""></p><p>可见,除了分行计算只涉及该列格子,一次列宽操作几乎涉及全表内容的计算,在大表下可能会导致几秒的卡顿,在一些低性能的机器上甚至会达到十几秒。由于该过程为同步计算,网页会表现为无响应,甚至严重的情况下会弹窗提示。</p><h2 id="计算过程优化"><a href="#计算过程优化" class="headerlink" title="计算过程优化"></a>计算过程优化</h2><p>之前我在<a href="https://godbasin.github.io/2022/06/04/front-end-performance-no-responding/">《前端性能优化–卡顿篇》</a>一文中,有详细介绍对于大任务计算的优化方向,包括:</p><ol><li>赋值和取值的优化。</li><li>优化计算性能和内存,包括使用享元的方式来优化数据存储,减少内存占用<br>及时地清理不用的资源,避免内存泄露等问题。</li><li>计算大任务进行拆解。</li><li>引入其他技术来加快计算过程,比如 Web Worker、WebAssembly、AOT 技术等。</li></ol><p>对于较大型的前端应用,即使并非使用 Canvas 自行排版,依然可能会面临计算耗时过大的计算任务。当然,更合理的方式是将这些计算放在后台进行,直接将计算完的结果给到前端使用。</p><p>也有一些场景,尤其是前端与用户交互很重的情况下,比如游戏和重编辑的产品。这类产品无法将计算任务放置在后端,甚至无法将计算任务拆分到 Web Worker 进行计算,因为请求的等待耗时、Worker 的通信耗时都会影响用户的体验。</p><p>对该类产品,最简单又实用的方法便是:拆。</p><h3 id="将计算任务做拆分"><a href="#将计算任务做拆分" class="headerlink" title="将计算任务做拆分"></a>将计算任务做拆分</h3><p>将计算任务做拆分,我们可以结合计算场景做分析,比如:</p><ul><li>只加载和计算最少的资源,比如首屏的数据</li><li>只进行可视范围内的计算和渲染更新,在可视区域外的则做异步计算或是暂不计算</li><li>支持增量计算和渲染,即仅变更的局部内容的重新计算和渲染</li><li>支持降级计算,对计算任务做优先级拆分,在用户机器性能差的情况下考虑降级渲染,根据优先顺序先后计算和绘制</li><li>设计任务调度器,对计算任务做拆分,并设计优先级进行调度</li></ul><p>比如,React16 中新增了调度器(Scheduler),调度器能够把可中断的任务切片处理,能够调整优先级,重置并复用任务。</p><p>调度器会根据任务的优先级去分配各自的过期时间,在过期时间之前按照优先级执行任务,可以在不影响用户体验的情况下去进行计算和更新。</p><p>通过这样的方式,React 可在浏览器空闲的时候进行调度并执行任务。</p><h3 id="预计算-异步计算"><a href="#预计算-异步计算" class="headerlink" title="预计算/异步计算"></a>预计算/异步计算</h3><p>还有一种同样常见的方式,便是将计算任务进行拆分后,通过预判用户行为,提前执行将用到的计算任务。</p><p>举个例子,当前屏幕内的数据都已计算和渲染完毕,页面加载处于空闲时,可以提前将下一屏幕的资源获取,并进行计算。</p><p>这种预计算和渲染的方式,有些场景下也会称之为离屏渲染。离屏渲染同样可以作用于 Canvas 绘制过程,比如使用两个 Canvas 进行交替绘制,或是使用 worker 以及浏览器提供的 OffscreenCanvas API,提前将要渲染的内容计算并渲染好,等用户进入下一屏的时候可以直接拿来使用。</p><p>如果是页面滚动的场景,还可以考虑复用滚动过程中重复的部分内容,来节省待计算和渲染的任务数量。</p><p>这些方案,我们后面都会详细进行一一讨论,本文就不过多描述了。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>或许很多开发同学都会觉得,以前没有接触过大型的前端项目,或是重交互重计算的产品,如果遇到了自己不知道该怎么做优化。</p><p>实际上,大多数的优化思路都是相似的,但是我们需要尝试跨越模板,将其应用在不同的场景下,你就会发现能得到许多想象以外的优化效果。</p><p>纸上得来终觉浅,绝知此事要躬行。不要自己把自己局限住了哟~</p>]]></content>
<summary type="html">
<p>前面<a href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/">《收集与渲染》</a>一文中,我们简单提到说在一些复杂场景下,从服务端获取的数据还需要进行计算,比如依赖 Web 浏览器的计算,亦或是游戏引擎中的碰撞检测。</p>
<p>本文我们详细针对复杂计算的场景来考虑渲染引擎的优化。</p>
</summary>
<category term="前端解决方案" scheme="https://godbasin.github.io/categories/%E5%89%8D%E7%AB%AF%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
<category term="性能优化" scheme="https://godbasin.github.io/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
</feed>