forked from godbasin/godbasin.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
499 lines (292 loc) · 281 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
<?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>2024-06-04T14:00:28.694Z</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/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>
<entry>
<title>复杂渲染引擎架构与设计--3.底层渲染适配</title>
<link href="https://godbasin.github.io/2023/07/19/render-engine-bottom-render-architecture/"/>
<id>https://godbasin.github.io/2023/07/19/render-engine-bottom-render-architecture/</id>
<published>2023-07-19T03:07:52.000Z</published>
<updated>2023-07-19T03:08:27.277Z</updated>
<content type="html"><![CDATA[<p>前面我们介绍了复杂渲染引擎中,使用的收集和渲染、以及插件等架构设计。至于底层具体的绘制实现,前面提到的多是 Canvas,实际上我们还可以适配不同的绘制引擎。</p><span id="more"></span><h2 id="多渲染方式适配"><a href="#多渲染方式适配" class="headerlink" title="多渲染方式适配"></a>多渲染方式适配</h2><p>关于渲染引擎整体架构和插件架构的设计,已在<a href="https://godbasin.github.io/front-end-playground/front-end-basic/render-engine/render-engine-render-and-collect.html">《收集与渲染》</a>、<a href="https://godbasin.github.io/front-end-playground/front-end-basic/render-engine/render-engine-plugin-design.html">《插件的实现》</a>两篇文章中介绍过,渲染引擎架构如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-plugin-design-1.jpg" alt=""></p><p>底层渲染引擎由收集器和渲染器组成,其中收集器收集需要渲染的渲染数据,渲染器则负责将收集到的数据进行直接渲染。</p><p>本文我们将会介绍渲染器的多种渲染方式的适配,其中常见的就包括:</p><ul><li>Canvas 渲染</li><li>SVG 渲染</li><li>DOM 渲染</li><li>其他渲染方式(如 WEBGL 渲染等)</li></ul><h3 id="适配架构设计"><a href="#适配架构设计" class="headerlink" title="适配架构设计"></a>适配架构设计</h3><p>对于多种渲染方式的适配,架构设计上还比较简单:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-bottom-render-architecture-1.jpg" alt=""></p><p>从收集器收集到的数据,通过适配的方式,转换成不同的绘制结果。举个例子,同样是一个单元格内容:</p><ul><li>Canvas 将需要分别绘制单元格背景(矩形)、单元格边框(线段)、单元格内容(文本)</li><li>SVG 与 Canvas 相似,但 SVG 需要注意元素的层级关系,组合成单元格</li><li>DOM 则可以通过一个<code><div></code>元素或是<code><table>/<tr>/<td></code>等表格元素来绘制</li></ul><p>一般来说,我们如果使用多种渲染方式,还需要考虑渲染一致性。渲染一致性是指,使用 Canvas 绘制的结果,需要与 SVG、DOM 绘制渲染的结果保持一致,不能出现太大的跳动或是位置、样式不一致的结果。</p><p>因此,我们在进行渲染的时候,根据选择的渲染方式,还需要做不同的兼容适配。</p><p>以上几种渲染方式中,DOM 渲染会受浏览器自身的排版引擎影响,这种影响可能是正面的,也可能是负面的。比如,我们 Canvas 排版方式是尽量接近浏览器原生的方式,那么当我们适配 DOM 渲染的时候则比较省力气。但如果说像在线表格这种场景,使用 DOM 进行表格的排版,则可能会遇到比较多的问题。</p><p>举个例子,我们都知道 DOM 里的表格元素(<code><table></code>/<code><tr></code>/<code><td></code>等)是最难驾驭的,因为浏览器对它们的处理总是在意料之外,宽高难以控制意味着我们将很难将其与 Canvas/SVG 的渲染效果对齐。因此,我们很可能需要在表格元素里嵌套绝对定位的<code><div></code>元素,来使得表格最终渲染不会被轻易撑开导致偏差。</p><p>除此之外,我们还需要注意文字的排版、换行等情况在 Canvas/SVG 和 DOM 渲染中需要尽量保持一致。</p><h3 id="各种渲染方式的选择"><a href="#各种渲染方式的选择" class="headerlink" title="各种渲染方式的选择"></a>各种渲染方式的选择</h3><p>每种渲染方式都有各自的优缺点。</p><p>在图表渲染引擎中,最常见的是 Canvas 渲染和 SVG 渲染,我们也可以从 ECharts 官网中找到两者的对比描述:</p><blockquote><ol><li>一般来说,Canvas 更适合绘制图形元素数量较多(这一般是由数据量大导致)的图表(如热力图、地理坐标系或平行坐标系上的大规模线图或散点图等),也利于实现某些视觉特效。</li><li>但在不少场景中,SVG 具有重要的优势:它的内存占用更低(这对移动端尤其重要)、并且用户使用浏览器内置的缩放功能时不会模糊。</li></ol><p>选择哪种渲染器,可以根据软硬件环境、数据量、功能需求综合考虑:</p><ul><li>在软硬件环境较好,数据量不大的场景下,两种渲染器都可以适用,并不需要太多纠结</li><li>在环境较差,出现性能问题需要优化的场景下,可以通过试验来确定使用哪种渲染器。比如:<ul><li>在需要创建很多 ECharts 实例且浏览器易崩溃的情况下(可能是因为 Canvas 数量多导致内存占用超出手机承受能力),可以使用 SVG 渲染器来进行改善</li><li>如果图表运行在低端安卓机,或者我们在使用一些特定图表如水球图等,SVG 渲染器可能效果更好</li><li>数据量较大(经验判断 > 1k)、较多交互时,建议选择 Canvas 渲染器</li></ul></li></ul></blockquote><p>而在在线表格的场景,我们会发现不同的团队会选择不同的渲染方式:</p><ul><li>谷歌表格使用了 Canvas/DOM 两种渲染方式,其中 DOM 渲染主要用于首屏直出</li><li>金山表格使用了 Canvas/SVG 两种渲染方式,其中 SVG 渲染主要用于首屏直出</li><li>飞书表格使用了 Canvas 渲染</li></ul><p>其实我们可以发现,这些团队很多在使用几种渲染方式,原因几乎都是因为使用了 Canvas 绘制作为主要渲染方式。但考虑到首屏渲染的情况,Canvas 则需要一系列的数据计算和渲染过程,不适合首屏直出的方式,因此会适配上 DOM 或者 SVG 进行首屏直出。</p><p>实际上,Canvas 渲染有一个比较致命的弱点:交互性很差。比如用户选择某个格子,进行拖拽、调整宽高、右键菜单等操作,在 Canvas 上是很难命中具体的元素的。因为 Canvas 绘制过程中并不像 DOM 和 SVG 一样有层次结构,最终的渲染结果也只是一个图像。因此,在线表格场景下大多数 Canvas 绘制都需要结合 DOM 引擎一起,获取到用户选择的元素、处理用户交互事件,然后进行二次计算和响应。</p><p>关于首屏直出,后面有空也可以简单唠唠。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文介绍了渲染引擎架构中,使用多种渲染方式以及底层渲染器适配的设计。</p><p>我们常常说给项目选择最优的解决方案,实际上我们也会发现,正因为往往没有所谓最优解,这些产品才会针对不同的场景下提供了不同的解决办法。比如,考虑到性能问题 ECharts 提供了 Canvas/SVG 两种绘制方式;又比如考虑到首屏直出的效率,各个在线表格的团队分别适配了更合适的渲染方式。</p>]]></content>
<summary type="html">
<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>大型前端项目的常见问题和解决方案</title>
<link href="https://godbasin.github.io/2023/07/01/complex-front-end-project-solution/"/>
<id>https://godbasin.github.io/2023/07/01/complex-front-end-project-solution/</id>
<published>2023-07-01T11:38:33.000Z</published>
<updated>2023-07-01T11:39:36.532Z</updated>
<content type="html"><![CDATA[<p>或许你会感到疑惑,怎样的项目算是大型前端项目呢?我自己的理解是,项目的开发人员数量较多(10 人以上?)、项目模块数量/代码量较多的项目,都可以理解为大型前端项目了。</p><p>在前端业务领域中,除了大型开源项目(热门框架、VsCode、Atom 等)以外,协同编辑类应用(比如在线文档)、复杂交互类应用(比如大型游戏)等,都可以称得上是大型前端项目。对于这样的大型前端项目,我们在开发中常常遇到的问题包括:</p><ol><li>项目代码量大,不管是编译、构建,还是浏览器加载,耗时都较多、性能也较差。</li><li>各个模块间耦合严重,功能开发、技术优化、重构工作等均难以开展。</li><li>项目交互逻辑复杂,问题定位、BUG 修复等过程效率很低,需要耗费不少精力。</li><li>项目规模太大,每个人只了解其中一部分,需求改动到不熟悉的模块时常常出问题。</li></ol><p>其实大家也能看到,大型前端项目中主要的问题便是“管理混乱”。所以我个人觉得,对于代码管理得很混乱的项目,你也可以认为是“大型”前端项目(笑)。</p><h2 id="问题-1:项目代码量过大"><a href="#问题-1:项目代码量过大" class="headerlink" title="问题 1:项目代码量过大"></a>问题 1:项目代码量过大</h2><p>对于代码量过大(比如高达 30W 行)的项目,如果不做任何优化直接全量跑在浏览器中,不管是加载耗时增加导致用户等待时间过久,还是内存占用过高导致用户交互卡顿,都会给用户带来不好的体验。</p><p>性能优化的解决方案在<a href="https://godbasin.github.io/2022/03/06/front-end-performance-optimization/">《前端性能优化–归纳篇》</a>一文中也有介绍。其中,对于代码量、文件过多这样的性能优化,可以总结为两个字:</p><ul><li><strong>拆</strong>:拆模块、拆公共库、拆组件库</li><li><strong>分</strong>:分流程、分步骤</li></ul><p>项目代码量过大不仅仅会影响用户体验,对于开发来说,代码开发过程中同样存在糟糕的体验:由于代码量过大,开发的本地构建、编译都变得很慢,甚至去打水 + 上厕所回来之后,代码还没编译完。</p><p>从维护角度来看,一个项目的代码量过大,对开发、编译、构建、部署、发布流程都会同样带来不少的压力。因此除了浏览器加载过程中的代码拆分,对项目代码也可以进行拆分,一般来说有两种方式:</p><p><strong>1. multirepo,多仓库模块管理,通过工作流从各个仓库拉取代码并进行编译、打包</strong>。</p><ul><li>优点:模块可根据需要灵活选择各自的编译、构建工具;每个仓库的代码量较小,方便维护</li><li>缺点:项目代码分散在各个仓库,问题定位困难(使用<code>npm link</code>有奇效);模块变动后,需要更新相关仓库的依赖配置(使用一致的版本控制和管理方式可减少这样的问题)</li></ul><p><strong>2. monorepo,单仓库模块管理,可使用 lerna 进行包管理</strong>。</p><ul><li>优点:项目代码可集中进行管理,使用统一的构建工具;模块间调试方便、问题定位和修复相对容易</li><li>缺点:仓库体积大,对构建工具和机器性能要求较高;对项目文件结构和管理、代码可测试和维护性要求较高;为了保证代码质量,对版本控制和 Git 工作流要求更高</li></ul><p>两种包管理模式各有优劣,一般来说一个项目只会采用其中一种,但也可以根据具体需要进行调整,比如统一的 UI 组件库进行分仓库管理、核心业务逻辑在主仓库内进行拆包管理。</p><blockquote><p>题外话:很多人常常在争论到底是单仓好还是多仓好,个人认为只要能解决开发实际痛点的仓,都是好仓,有时候过多的理论也需要实践来验证。</p></blockquote><h2 id="问题-2:模块耦合严重"><a href="#问题-2:模块耦合严重" class="headerlink" title="问题 2:模块耦合严重"></a>问题 2:模块耦合严重</h2><p>不同的模块需要进行分工和配合,因此相互之间必然会产生耦合。在大型项目中,由于模块数量很多(很多时候也是因为代码量过多),常常会遇到模块耦合过于严重的问题:</p><ol><li>模块职责和边界定义不清晰,导致模糊的工作可能存在多个模块内。</li><li>各个模块没有统一管理,导致模块在状态变更时需要手动通知相关模块。</li><li>模块间的通信方式设计不合理,导致全局事件满天飞、A 模块内直接调用 B 模块等问题,隐藏的引用和事件可能导致内存泄露。</li></ol><p>对于模块耦合严重的模块,常见的解耦方案比如:</p><ul><li>使用事件驱动的方式,通过事件来进行模块间通信</li><li>使用依赖倒置进行依赖解耦</li></ul><h3 id="事件驱动进行模块解耦"><a href="#事件驱动进行模块解耦" class="headerlink" title="事件驱动进行模块解耦"></a>事件驱动进行模块解耦</h3><p>使用事件驱动的方式,可以快速又简单地实现模块间的解耦,但它常常又带来了更多的问题,比如:</p><ul><li>全局事件满天飞,不知道某个事件来自哪里,被多少地方监听了</li><li>无法进行事件订阅的销毁管理,容易存在内存泄露的问题</li><li>事件维护困难,增加和调整参数影响面广,容易触发 bug</li></ul><h3 id="依赖倒置进行模块解耦"><a href="#依赖倒置进行模块解耦" class="headerlink" title="依赖倒置进行模块解耦"></a>依赖倒置进行模块解耦</h3><p>我们还可以使用依赖倒置进行依赖解耦。依赖倒置原则有两个,包括:</p><ol><li>高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。</li><li>抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口。</li></ol><p>使用以上方式进行设计的模块,不会依赖具体的模块和细节,只按照约定依赖抽象的接口。</p><p>如果项目中有完善的依赖注入框架,则可以使用项目中的依赖注入体系,像 Angular 框架便自带依赖注入体系。依赖注入在大型项目中比较常见,对于各个模块间的依赖关系管理很实用,比如 VsCode 中就有使用到依赖注入。</p><h3 id="VsCode:结合事件驱动与依赖倒置进行模块解耦"><a href="#VsCode:结合事件驱动与依赖倒置进行模块解耦" class="headerlink" title="VsCode:结合事件驱动与依赖倒置进行模块解耦"></a>VsCode:结合事件驱动与依赖倒置进行模块解耦</h3><p>在 VsCode 中,我们也可以看到使用了依赖注入框架和标准化的<code>Event/Emitter</code>事件监听的方式,来对各个模块进行解耦(可参考<a href="https://godbasin.github.io/front-end-playground/front-end-basic/deep-learning/vscode-event.html">《VSCode 源码解读:事件系统设计》</a>):</p><ul><li>各个模块的生命周期(初始化、销毁)统一由框架进行管理:通过提供通用类<code>Disposable</code>,统一管理相关资源的注册和销毁</li><li>模块间不直接引入和调用,而是通过声明依赖的方式,从框架中获取相应的服务并使用</li><li>不直接使用全局事件进行通信,而是通过订阅具体服务的方式来处理:通过使用同样的方式<code>this._register()</code>注册事件和订阅事件,将事件相关资源的处理统一挂载到<code>dispose()</code>方法中</li></ul><p>使用依赖注入框架的好处在于,各个模块之间不会再有直接联系。模块以服务的方式进行注册,通过声明依赖的方式来获取需要使用的服务,框架会对模块间依赖关系进行分析,判断某个服务是否需要初始化和销毁,从而避免了不必要的服务被加载。</p><p>在对模块进行了解耦之后,每个模块都可以专注于自身的功能开发、技术优化,甚至可以在保持对外接口不变的情况下,进行模块重构。</p><p>实际上,在进行代码编程过程中,有许多设计模式和理念可以参考,其中有不少的内容对于解耦模块间的依赖很有帮助,比如接口隔离原则、最少的知识原则/迪米特原则等。</p><p>除了解决问题,还要思考如何避免问题的发生。对于模块耦合严重这个问题,要怎么避免出现这样的情况呢?其实很依赖项目管理的主动意识和规范落地,比如:</p><ol><li>项目规模调整后,对现有架构设计进行分析,如果不再合适则需要进行及时的调整和优化。</li><li>使用模块解耦的技术方案,将各个模块统一交由框架处理。</li><li>梳理各个模块的职责,明确每个模块负责的工作和提供的功能,确定各个模块间的边界和调用方式。</li></ol><h2 id="问题-3:问题定位效率低"><a href="#问题-3:问题定位效率低" class="headerlink" title="问题 3:问题定位效率低"></a>问题 3:问题定位效率低</h2><p>在对模块进行拆分和解耦、使用了模块负责人机制、进行包拆分管理之后,虽然开发同学可以更加专注于自身负责模块的开发和维护,但有些时候依然无法避免地要接触到其它模块。</p><p>对于这样大型的项目,维护过程(熟悉代码、定位问题、性能优化等)由于代码量太多、各个函数的调用链路太长,以及函数执行情况黑盒等问题,导致问题定位异常困难。要是遇到代码稍微复杂点,比如事件反复横跳的,即使使用断点调试也能看到眼花,蒸汽眼罩都得多买一些(真的贵啊)。</p><p>对于这些问题,其实可以有两个优化方式:</p><ol><li>维护模块指引文档,方便新人熟悉现有逻辑。文档主要介绍每个模块的职责、设计、相关需求,以及如何调试、场景的坑等。</li><li>尝试将问题定位过程进行自动化实现,比如模块负责人对自身模块执行的关键点进行标记(使用日志或者特定的断点工具),其他开发可根据日志或是通过开启断点的方式来直接定位问题</li></ol><p>这个过程,其实是将模块负责人的知识通过工具的方式授予其他开发,大家可以快速找到某个模块经常出问题的地方、模块执行的关键点,根据建议和提示进行问题定位,可极大地提升问题定位的效率。</p><p>除了问题定位以外,各个模块和函数的调用关系、调用耗时也可以作为系统功能和性能是否有异常的参考。之前这块我也有简单研究过,可以参考<a href="https://godbasin.github.io/2020/06/21/trace-stash/">《大型前端项目要怎么跟踪和分析函数调用链》</a>。</p><p>因此,我们还可以通过将调用堆栈收集过程自动化、接入流水线,在每次发布前合入代码时执行相关的任务,对比以往的数据进行分析,生成系统性能和功能的风险报告,提前在发布前发现风险。</p><h2 id="问题-4:项目复杂熟悉成本过高"><a href="#问题-4:项目复杂熟悉成本过高" class="headerlink" title="问题 4:项目复杂熟悉成本过高"></a>问题 4:项目复杂熟悉成本过高</h2><p>即使在项目代码量大、项目模块过多、耦合严重的情况下,项目还在不断地进行迭代和优化。遇到这样的项目,基本上没有一个人能熟悉所有模块的所有细节,这会带来一些问题:</p><ul><li>对于新需求、新功能,开发无法完整地评估技术方案是否可以实现、会不会带来新的问题</li><li>需求开发时需要改动不熟悉的代码,无法评估是否存在风险</li><li>架构级别的优化工作,难以确定是否可以真正落地</li><li>一些模块遗留的历史债务,由于工作进行过多次交接,相关逻辑已无人熟悉,无法进行处理</li></ul><p>导致这些问题的根本原因有两个:</p><ul><li>开发无法专注于某个模块开发</li><li>同一个模块可能被多个人调整和变更</li></ul><p>对于这种情况,可以使用模块负责人的机制来对模块进行所有权分配,进行管理和维护:</p><ol><li>每个开发都认领(或分配)一个或多个模块,并要求完全熟悉和掌握模块的细节,且维护文档进行说明。</li><li>对于需求开发、BUG 修复、技术优化过程中涉及到非自身的模块,需要找到对应模块的负责人进行风险评估和代码 Review。</li><li>模块的负责人负责自身模块的技术优化方案,包括性能优化、自动化测试覆盖、代码规范调整等工作。</li><li>对于较核心/复杂的模块,可由多个负责人共同维护,协商技术细节。</li></ol><p>通过模块负责人机制,每个模块都有了对应的开发进行维护和优化,开发也可以专注于自身的某些模块进行功能开发。在人员离职和工作内容交接的时候,也可以通过文档 + 负责人权限的方式进行模块交接。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>大型项目的这些痛点,其实只是我们工作中痛点的缩影。技术上能解决的问题都是小事,管理和沟通上的事情才更让人头疼。</p><p>除此之外,在我们的日常工作中,通常也会局限于某块功能的实现和某个领域的开发。如果这些内容并没有足够的深度可以挖掘,对个人的成长发展也可能会有限制。在这种情况下,我们还可以主动去了解和学习其它领域的知识,也可以主动承担起更多的工作内容。</p>]]></content>
<summary type="html">
<p>或许你会感到疑惑,怎样的项目算是大型前端项目呢?我自己的理解是,项目的开发人员数量较多(10 人以上?)、项目模块数量/代码量较多的项目,都可以理解为大型前端项目了。</p>
<p>在前端业务领域中,除了大型开源项目(热门框架、VsCode、Atom 等)以外,协同编辑类应
</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>复杂渲染引擎架构与设计--2.插件的实现</title>
<link href="https://godbasin.github.io/2023/06/15/render-engine-plugin-design/"/>
<id>https://godbasin.github.io/2023/06/15/render-engine-plugin-design/</id>
<published>2023-06-15T06:23:21.000Z</published>
<updated>2023-06-15T06:23:07.422Z</updated>
<content type="html"><![CDATA[<p>上一篇<a href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/">《收集与渲染》</a>文章中,我们提到收集与渲染的拆分设计,对后续的业务拓展会更方便。</p><p>本文将介绍渲染插件的设计,渲染插件可用于各种新特性的拓展绘制。</p><span id="more"></span><h2 id="支撑业务的快速发展"><a href="#支撑业务的快速发展" class="headerlink" title="支撑业务的快速发展"></a>支撑业务的快速发展</h2><p>我们设计了渲染引擎,只能满足基础图形的收集和绘制,包括文本、线段、矩形、图像等。</p><p>而应用到具体的业务中,则是使用这些基础图形来绘制出业务相关的内容。因此,我们可以考虑将基础的能力进行封装,提供更便利的能力给到业务侧使用。</p><h3 id="分层封装底层能力"><a href="#分层封装底层能力" class="headerlink" title="分层封装底层能力"></a>分层封装底层能力</h3><p>举个例子,依然是表格的场景,由于大多数内容都是以单元格为基本单位来进行绘制的,我们则可以封装出一个提供按单元格绘制的中间层能力。</p><p>而当业务侧进行编辑操作时,更新的范围除了单个格子,也会包括整行、整列、整表、所选区域等情况,因此我们可以封装给到业务侧这些能力。</p><p>到这里,渲染引擎架构如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-plugin-design-1.jpg" alt=""></p><p>这种分层能力不仅在渲染引擎中可以用到,即使在我们平时的页面开发中,也完全可以用到。最常见的包括将页面布局做分层拆分,然后进行渲染。</p><p>不过现在基本上渲染的流程和实现都交由前端框架来负责,而 DOM 的布局则都交给浏览器本身的排版引擎去处理,用 Canvas 来绘制布局的场景的确很少。</p><h3 id="业务侧插件的设计"><a href="#业务侧插件的设计" class="headerlink" title="业务侧插件的设计"></a>业务侧插件的设计</h3><p>除了给上层业务提供封装好的能力,业务侧可以指定单元格范围进行重新渲染以外,还需要考虑另外一种的业务拓展场景:业务需要在单元格内绘制自己的内容,比如单元格背景高亮、一些特殊的图形属性单元格、图片绘制等等。</p><p>所以我们还需要给业务提供控制单元格绘制内容的能力。</p><p>前面一篇文章我们提到,每个单元格的绘制会有堆叠顺序,比如先绘制背景色,再绘制文字、边框线等等。那么,如果我们要给业务侧提供绘制的能力,他们同样需要可控制的堆叠顺序,和绘制内容的控制。</p><p>既然我们将渲染过程分成了收集和渲染两部分,渲染器的能力可以说是通用的能力,因为不管是单元格本身的绘制,还是业务侧新增的绘制内容,都离不开最基本的文字绘制、线段绘制、图形绘制、图像绘制等能力。</p><p>因此,我们可以考虑在收集过程中,通过提供插件的能力,让业务侧把想要绘制的内容收集起来。</p><p>插件提供的能力包括:</p><ol><li>定义收集时机,比如指定单元格绘制时进行收集,或是按行、按列、全表绘制时进行收集。</li><li>定义绘制配置,比如指定单元格绘制时,阻断正常单元格绘制内容。举个例子,考虑某个单元格不需要正常渲染文字内容,而是需要渲染特定的图形内容(如评星、进度条等),则可以通过配置控制。</li><li>添加绘制内容到收集器,绘制内容可指定绘制类型(文字、线段、图形等)以及堆叠顺序。</li></ol><p>插件的收集流程大概如图:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-plugin-design-2.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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 该代码只是写了个思路,不作为最终的实现方式</span></span><br><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">BaseCollectPlugin</span> {</span><br><span class="line"> <span class="keyword">protected</span> <span class="attr">collectWhen</span>: <span class="string">'column'</span> | <span class="string">'row'</span> | <span class="string">'cell'</span> | <span class="string">'sheet'</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="attr">collectConfig</span>: {</span><br><span class="line"> <span class="attr">collectText</span>: <span class="function">() =></span> <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">collectBorder</span>: <span class="function">() =></span> <span class="built_in">boolean</span>;</span><br><span class="line"> <span class="attr">collectBackground</span>: <span class="function">() =></span> <span class="built_in">boolean</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="attr">collect</span>: <span class="function">() =></span> <span class="title class_">ICollectInfo</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="title function_">constructor</span>(<span class="params">dependency</span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">collectWhen</span> = dependency.<span class="property">collectWhen</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">collectConfig</span> = dependency.<span class="property">collectConfig</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">collect</span> = dependency.<span class="property">collect</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过这样的方式,业务可以自由地控制某些范围内单元格的绘制内容,且不需要侵入性地修改核心的绘制流程,拓展性得到了很好的提升。</p><p>现在越来越多的软件都支持通过插件的方式来拓展能力,也允许开发者一起来打造插件体系。对于插件的设计来说,独立性、安全性、拓展性都是比较重要的考虑方向。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文结合收集和渲染的渲染架构,设计了一套方便业务拓展的底层能力,包括提供支持可选范围的重新渲染能力,以及控制单元格绘制内容的插件能力。</p><p>很多时候我们都关注核心的架构能力,而往往忽略了业务的快速发展和迭代。实际上,架构就是为了不断变化的业务服务的,因此架构设计的时候,保留符合业务发展需要的拓展能力也是十分必要的。</p>]]></content>
<summary type="html">
<p>上一篇<a href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/">《收集与渲染》</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>复杂渲染引擎架构与设计--1.收集与渲染</title>
<link href="https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/"/>
<id>https://godbasin.github.io/2023/05/13/render-engine-render-and-collect/</id>
<published>2023-05-13T01:52:22.000Z</published>
<updated>2023-05-13T01:52:52.299Z</updated>
<content type="html"><![CDATA[<p>对于一般的渲染引擎来说,我们可以简单地拿到待渲染的数据,然后直接通过 Canvas/DOM/SVG 来将需要渲染的图形和内容渲染出来。</p><p>而在复杂场景下,比如需要自行排版的文本、表格和图形类,光是将要渲染的数据计算出来,便容易面临性能瓶颈,而对于样式多样、结构复杂的内容来说,绘制过程也同样会出现性能瓶颈。</p><span id="more"></span><p>本文我们主要针对 Canvas 绘制的场景,来考虑将绘制的流程分为收集和渲染两部分。</p><h2 id="渲染数据的收集"><a href="#渲染数据的收集" class="headerlink" title="渲染数据的收集"></a>渲染数据的收集</h2><p>很多时候,我们在后台数据库存储的只有图形的基本信息,对于需要排版计算的数据来说,则需要在拿到数据之后,再根据页面进行排版计算,完成后才能渲染到页面。</p><p>或许这样说会有些抽象,我们以表格的渲染为例来说明。</p><p>对于表格这样的产品来说,存储的往往是以单元格为基本单位的数据,如每个单元格的内容(可能是复杂的富文本、图片、图标结合)、样式(边框、背景色)、行列的宽高等。而在实际上页面渲染的时候,我们可能会根据行列宽高、每个单元格的边框线设置来绘制格子的布局。</p><p>除此之外,我们还可能需要考虑单元格内容是否会超出单元格,来判断是否需要截断渲染、是否需要换行显示等。这便要求我们需要对内容宽高进行测量,比如使用<code>CanvasRenderingContext2D.measureText()</code>测量文本宽度,这依赖了浏览器环境下的 API 能力。这意味着我们无法在后台提前计算好这些数据,因此无法通过提前计算来加速渲染过程。</p><p>于是,我们需要在前端拿到后台数据后,再进行相应的排版计算。</p><p>如果说一边计算一边绘制,则整个过程的耗时会比较长,用户也可能会看到绘制过程,该体验不是很友好。因此,我们可以在计算过程中,先把要最终绘制的数据结果先收集到一起,Canvas 绘制的时候则可以直接用。</p><h2 id="收集与绘制的功能划分"><a href="#收集与绘制的功能划分" class="headerlink" title="收集与绘制的功能划分"></a>收集与绘制的功能划分</h2><p>我们可以根据绘制内容,划分为以下的收集器和渲染器:</p><ul><li>线段数据收集和绘制(如表头、边框线等)</li><li>矩形数据收集和绘制(如背景色)</li><li>图像数据收集和绘制(如图片)</li><li>文本数据收集和绘制(如文字内容)</li><li>其他数据收集和绘制</li></ul><p>可见,收集器和渲染器的类型是一一对应的,渲染器在渲染的时候,可以直接从对应的收集器类型中获取数据,然后绘制。</p><p>还需要考虑一种情况,即相同的收集器和不同的收集器类型里,绘制内容有重叠时,需要考虑绘制堆叠的顺序。举个例子,单元格的文字需要在背景色上面,也就是说单元格的绘制需要比背景色要晚。</p><p>这意味着我们在收集的时候,还需要给收集的数据添加堆叠顺序,在绘制的时候,则按照堆叠顺序先后绘制。我们可以将收集器分为多个,每个收集器定义堆叠顺序,相同堆叠顺序的数据收集到一起。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-render-and-collect-1.jpg" alt=""></p><p>其实,我们使用一个收集器,通过给数据添加渲染类型,来将不同类型的数据放在一起,方便统一管理。在渲染的时候,则先根据绘制类型和堆叠顺序进行排序,再进行绘制。</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/render-engine-render-and-collect-2.jpg" alt=""></p><h2 id="渲染数据享元"><a href="#渲染数据享元" class="headerlink" title="渲染数据享元"></a>渲染数据享元</h2><p>前面我们在<a href="">《前端性能优化–Canvas 篇》</a>一文中描述过,Canvas 上下文切换的成本消耗比较大,如果在复杂内容绘制的情况下,可能会导致性能问题。</p><p>使用收集器的一个好处是,我们可以将同类型同样式的渲染数据进行享元。对于样式完全一样的数据,收集器可通过对样式进行 hash 享元存储在一起。</p><p>这样绘制的时候,就可以将样式一样的内容一起绘制,减少 Canvas 上下文的切换。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LineCollector</span> {</span><br><span class="line"> <span class="comment">// 以线段样式为 key,存储线段的开始和结束位置,如 [[startX, startY], [endX, endY]]</span></span><br><span class="line"> <span class="keyword">private</span> <span class="attr">renderInfos</span>: { [<span class="attr">key</span>: <span class="built_in">string</span>]: <span class="built_in">number</span>[][] } = {};</span><br><span class="line"></span><br><span class="line"> <span class="title function_">addRenderInfo</span>(<span class="params">renderInfo</span>) {</span><br><span class="line"> <span class="keyword">const</span> hash = <span class="variable language_">this</span>.<span class="title function_">generateKey</span>(renderInfo);</span><br><span class="line"> <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">renderInfos</span>[hash]) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">renderInfos</span>[hash] = [];</span><br><span class="line"> }</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">renderInfos</span>[hash].<span class="title function_">push</span>([...renderInfo.<span class="property">position</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_">generateKey</span>(<span class="attr">lineProp</span>: { <span class="attr">color</span>: <span class="built_in">string</span>; <span class="attr">thickness</span>: numer }): <span class="built_in">string</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<span class="subst">${lineProp.thickness}</span>-<span class="subst">${lineProp.color}</span>`</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过这样的方式,我们将要绘制的数据收集起来,方便 Canvas 进行更高效的渲染。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>通过将 Canvas 渲染过程拆分为收集和渲染两部分,架构上更清晰的同时,在一定程度上提升了渲染的性能和效率。而这样的架构设计,也更方便后续做更多的拓展,我会在后续篇章继续介绍。</p>]]></content>
<summary type="html">
<p>对于一般的渲染引擎来说,我们可以简单地拿到待渲染的数据,然后直接通过 Canvas/DOM/SVG 来将需要渲染的图形和内容渲染出来。</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>如何进行前端职业规划</title>
<link href="https://godbasin.github.io/2023/04/06/front-end-career-planning/"/>
<id>https://godbasin.github.io/2023/04/06/front-end-career-planning/</id>
<published>2023-04-06T12:02:28.000Z</published>
<updated>2023-04-06T12:02:40.065Z</updated>
<content type="html"><![CDATA[<p>我们还没离开校园的时候,就已经知道要对自己未来的职业发展进行规划。但并没有人会来教会我们这些,而我们在一股脑扎进工作里以后,又常常因为忙也好因为懒也好,觉得目前状态还可以,就把职业规划也扔脑后了。</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><p>很多时候,即使我们已经确认想要去往哪个方向,但实际上依然会被未来的某些事物改变。正如很多公司要求员工写 KPI,员工将 KPI 写得再详细,依然在半年后考核时需要重新修改,因为计划永远赶不上变化。但我们不能因为未来可能会变,就认为没有意义,也不去写 KPI。</p><p>这就好比我们在做习题本,本子的最后写好了答案,既然都知道自己最后都会看答案,那么我们做不做题、是否做错了都无关紧要了吗?显然不是的,做题是个需要思考计算的过程,通过最终的答案我们可以知道自己的思考方式是否有误、是否需要调整。写 KPI 也是一个道理,我们如果最初定的 KPI 与最终的不一样,是否需要去反思下为什么呢?KPI 本身的设计,不就是为了让我们确认自己规划做的事情,最终是否实现了吗?</p><p>因此,即使未来某个时候我们的职业方向会进行调整,此时此刻我们一样需要对此进行规划。通过规划我们才有了目标,有了目标,我们才会制定计划并为之努力实现。</p><h3 id="扬长避短很有效"><a href="#扬长避短很有效" class="headerlink" title="扬长避短很有效"></a>扬长避短很有效</h3><p>其实我们每个人也都有各自擅长的部分,一般来说工作中更有效的方式是扬长避短,而不是花很多时间去补齐短板。我们常常会听到其他人说,“你没做过这些、要去挑战自己”,挑战自己的确是需要的,多尝试去做一些事情也总是好的。</p><p>如果我们需要在这场竞争中生存下来,必须拥有自己的不可替代性,也称之为技术壁垒。如果我们有擅长的领域,不妨尝试去深挖这样的领域,这样是一个相对容易的选择。</p><p>很多时候,也正是因为热爱和喜欢这些领域,我们才愿意投入更多的精力,因此在某种程度上也会更擅长。而做自己喜欢的事情的时候,即使再忙再苦再累,也一样会觉得很开心和值得。相反,如果做着不喜欢的工作时,每时每刻都是一种煎熬,每天上班就像去上坟。</p><p>可以的话,找一个可以发挥你自身优势的地方,那样的你会闪闪发光,你也会因此爱上你自己。</p><h3 id="你的出处并不能代表什么"><a href="#你的出处并不能代表什么" class="headerlink" title="你的出处并不能代表什么"></a>你的出处并不能代表什么</h3><p>我刚进入互联网的时候,非科班出身、缺乏开发经验,自学了一周多就疯狂投简历。找工作很头疼,即使是裸辞的一番热血,再热烈也很容易被浇灭。最后虽然顺利找到了工作,但工资少得可怜。</p><p>那会从华为出来,待遇的落差总会不断地提醒我,到底是为了什么呢?但满怀的热情使得我每天上班充满干劲,下班后也继续在床上打着灯看书和写代码学习。那是 jQuery 横行的年代,似乎只需要掌握了它,你就能所向披靡。还有 CSS/CSS3 动画等,对 CSS 的掌握基本上是 10%的理解加上 90%的实践日积月累不断沉淀的。</p><p>那段时间可以感觉到成长很快,几位后台开发小哥哥带着入门,告诉我需要学习哪些知识、可以去哪些网站上学,然后就停不下来了。我曾经在面试的时候说自己学习能力很强,但是通常别人会问,你怎么证明呢?</p><p>不知道为什么,现在似乎大家多多少少都会不正视外包,“要不是能力不够也不会当外包”、“不能指望他们能学会什么”这样的话也经常会听到。可能是因为很多人的经历和体验里,都是比较顺畅阻力较少。而我也很荣幸曾经置于职业的低谷,使得很多时候能看到更多的可能性。</p><p>同样的,如今很多公司在招人的时候对学历的要求也越来越高,这是由于竞争市场资源过剩导致的,公司或许在性价比等方面考虑进行这样的调整,但我们不能自己限制住自己。我也见过一些特别厉害的开发,他们并不一定来自很好的学校。只是因为有着一股认真钻研的劲,他们看不到所谓的比较、竞争,专心致志地沉浸在自己的世界中,并做出了很多的成绩。</p><p>是否科班出身、是否外包出身、是否学历优秀,这些或许对其他人来说会有影响,但这些都不应该影响我们自身要去努力,不应该成为我们去给自身贴上一些标签的理由。</p><p>而如今大家都说什么寒冬到来、前端已死,要问我的想法,大概是世上无难事、只怕有心人罢了。</p><p>真正的热爱,从来不会因一份怎样的工作而受到影响,只不过如今大多数人更在意利益得失而已。</p><h3 id="将目标放长放远"><a href="#将目标放长放远" class="headerlink" title="将目标放长放远"></a>将目标放长放远</h3><p>或许你会觉得制定计划不靠谱,因为事情永远都在变化。工作总是很复杂,不可控制的因素太多了。例如小明想要深入钻研浏览器渲染的方向,但实际上团队负责的业务都比较简单,因此小明常常需要快速上线某个新模块,节奏一直慢不下来,也完全没有机会和精力去研究自己的东西。</p><p>遇到这种情况,可以尝试将眼光再放长远一些。如果将职业规划比作一场马拉松,首先我们得确认目标是什么,是拿到前面的名次还是完成这场比赛?以前参加过中长跑的田径比赛,教练在比赛前会叮嘱我们几件事:</p><ol><li>起跑的时候不要用力过猛,但需要让自己保持在一定的名次以内。 </li><li>过程中可以选择排在前面节奏比较好的选手,紧跟在对方后面。领跑比跟跑更累,紧紧贴在对方身后可以让对方心理上有压力。 </li><li>除了最后的冲刺,最好要保持匀速的节奏,因为变速跑会消耗更多的体力。 </li></ol><p>其实做职业规划和长跑很相似,我们需要确定一个较远的目标,然后控制好节奏、切忌着急和用力过猛。以我自己为例,一开始我想要往前端深度的领域发展,但我的起跑线是非科班+外包。显然,我无法一下子到达自己想去的地方,而此时自身的实力也无法和想要的岗位相匹配,因此我整体的职业路线是:外包公司前端 -> 中规模公司前端 -> 大公司重后台的业务部门前端 -> 大公司重前端业务部门前端。</p><p>这是一个经历了好些年的过程,每当我觉得在所在的团队中没法获得更多成长的时候,就会选择进入下一个阶段。如果目的很明确的话,你会清晰地知道自己什么时候该调整、接下来要去哪。</p><p>中间也遇到过一些团队,虽然团队中缺乏我想要的复杂大型前端业务项目,但团队会给道其他的机会,例如待遇上的回报、往管理方向发展带团队等等。很多条件都比较诱人,团队也的确给到了足够的诚意希望我留下来。但我知道自己想要的是前端领域的深挖,如果现在因为有其它诱惑而暂时选择放弃,以后遇到同样的困境是不是每次都会选择放弃呢?</p><p>因此,基本上我都选择了按照原目标继续往前走。有管理者觉得很疑惑,大家都往钱多的地方走,他问我是不是对钱没什么需要。我当时的回答是,把该学的知识和技能都掌握,如何赚钱它不该是我需要担心的问题。</p><p>将眼光放远一些,直到我们建立起自己的技术壁垒,在那之前即使挣到更多的钱、可以带团队干活,也依然可能会面临被这个行业抛弃。当然,这里的前提是我自身想要往技术的方向去发展。如果想要往管理方向去发展,一些带团队的经验也是很有帮助的。</p><p>如果现在的你距离自己的目标太远,那么不妨尝试找到去向最终目标的一个小目标,先往小目标去努力。通往目标的路上干扰很多,但如果你足够地坚定,总有一天也能去到自己想去的地方的。</p><h2 id="职业发展中面临的选择"><a href="#职业发展中面临的选择" class="headerlink" title="职业发展中面临的选择"></a>职业发展中面临的选择</h2><p>对于每个技术人来说,都会遇到一些发展方向的选择困惑,比如“该往深度发展”还是“该往广度发展”。前端也不例外,我们的发展方向种类越来越多,但一个人的精力总是有限的,我们还是需要做出选择。</p><h3 id="全栈-or-纯前端?"><a href="#全栈-or-纯前端?" class="headerlink" title="全栈 or 纯前端?"></a>全栈 or 纯前端?</h3><p>如今随着 Node.js 的普及,也有不少的前端开发慢慢转型做全栈、大前端等方向。</p><p>的确,对于有全栈工作经验的人来说,找工作的时候会更吃香。但我们日常工作中是否都有机会去接触后台开发、客户端开发这些内容呢?是否一定需要有这样的工作经验才能获得更好的发展呢?</p><p>很多时候,前端由于门槛较低,很多的前端开发(比如我)都不是计算机专业出身。我们对于计算机基础、网络基础、算法和数据结构等内容掌握很少,更多时候是这些知识的缺乏阻碍了我们在程序员这一职业的发展,这也是为什么很多前端开发苦恼自己到达天花板,想着转型全栈或者后台就能走得更远。</p><p>这其实是个误区。后台开发由于开发语言、服务器管理、计算机资源等工作内容的不一致,对于专业基础的要求更高,因此看上去似乎比前端能走得更远。但随着成熟的解决方案的出现,像分布式部署和管理、全链路跟踪等,以及运维和 DBA 等职位的出现、后台基本框架的完善,更多的后台开发技术选型的范围不大,在开发过程中也同样会偏向业务开发,因此更多的关注点会落在业务风险梳理、问题定位和追踪、业务稳定性、效率提升等地方。对于全栈开发中的后台开发,可能涉及的内容会更加局限一些。</p><p>所以,其实我们在日常工作中也可以更多地关注后台的实现和能力,除了可以更好地配合和理解后台的工作外,还可以提升自己对后台工作内容的理解。当然,最重要的依然是扎实地补充计算机基础知识。</p><p>全栈开发经验可能让我们更容易地找到工作,但只有基础知识的掌握足够深入,才可以在接触后台开发、终端开发等内容的时候,有足够的能力去快速高效地解决问题。</p><p>相比转型全栈,其实纯前端可深挖的方向也很多,包括关注性能的各种深入的性能优化领域、关注效能的脚手架/CI/CD 等构建领域、关注可维护的项目与代码设计等架构领域、关注浏览器渲染的游戏引擎/WebGL 等特殊领域。选择走广度方向会要求有足够丰富的项目经验,而选择走深度方向只需要在某个领域有足够深刻的理解和突破,就可以建立起稳固的技术壁垒。</p><h3 id="ToB-or-ToC"><a href="#ToB-or-ToC" class="headerlink" title="ToB or ToC"></a>ToB or ToC</h3><p>对于前端同学来说,我们也常常会纠结与 ToB 和 ToC 的工作内容选择,它们之间区别多在于用户群体和数量。</p><p>一般来说,ToB 的业务服务于某一类用户群体,因此会根据服务对象的不一样,工作重点有所区别。例如,如果服务于银行,会对技术方案/安全性要求严格;如果服务于政府机构,则可能需要兼容较低版本的 IE 浏览器(笑),技术选型比较局限。但通常来说,ToB 业务的用户量并不会特别大,对性能要求较低,有些情况下也会由于机器部署环境封闭的原因,对网络和安全性要求较低,因此 ToB 业务可以更多关注开发效率提升、技术管理选型、项目可维护性等方面。</p><p>ToC 的业务用户量较大,对加载性能、浏览器兼容性等都要求很高,因此常常需要进行性能优化、兼容性检测、实时监控、SEO 优化等工作。</p><p>找工作的时候,拥有 ToC 业务开发经验通常会比拥有 ToB 业务开发经验的优势要大一点,因为 ToC 对前端的各个角度要求都相对较高。但在真正的工作中,由于精力和工作内容分配的问题,很多参与 ToC 业务的人更多只会关注到自己负责的模块部分,因此很多时候并没有掌握到较完整的 ToC 业务相关的关键技术方案。同样,即使是在做的是 ToB 业务,也有不少小伙伴会有很多时间去研究一些新技术、做很多的选型调研,也可以在这个过程中获得很好的成长。</p><p>所以,决定我们能否掌握更多的、成长更快的,或许业务的影响比我们想象中还要小,最终还是取决于自己。</p><h3 id="赚钱-or-个人成长?"><a href="#赚钱-or-个人成长?" class="headerlink" title="赚钱 or 个人成长?"></a>赚钱 or 个人成长?</h3><p>当一份很赚钱但没什么技术含量的工作(下面成为工作 A),以及一份有趣又具备足够挑战性的工作(工作 B),这样两份工作放在我们面前的时候,大概很多人都会犹豫。这的确是一个很现实的问题,钱可以让我们买到很多自己想要的东西,但它却没法买来成长极快的工作经历。丰富的工作经历可以给我们带来竞争力,但短期内可能会带来经济上的困扰。</p><h4 id="分析每个阶段的需求"><a href="#分析每个阶段的需求" class="headerlink" title="分析每个阶段的需求"></a>分析每个阶段的需求</h4><p>前面我有表达过自己的观点,如果我们掌握了足够的技术和能力,就不会担心自己赚不到钱。如果以这个角度来看,工作 B 显然会是我们的选择。但这个世界并不都是非黑即白的,如果我们身边大多数的小伙伴都选择了向钱看,他们每年赚到的钱甚至是我们的一两倍,大多数人都无法不为所动。当然,如果有一份工作又能成长又能赚钱的工作最好,但遇到这样的选择概率会很低。</p><p>我们可以将自己的职业发展分为几个阶段,然后针对每个阶段分析该阶段中最重要的一些目标,这些目标可以是自身能力的提升,也可以是工资的上涨,还可以是职级的晋升等等。那么,当我们遇到困扰的选择时,可以选择当前阶段中比较重要的目标相关的工作内容。</p><p>举个例子,张三家境不好,他希望毕业工作之后可以尽快帮家里还清贷款,那么张三可以先选择赚钱更多的工作 A。当贷款还清以后,张三可以进入下一个阶段,如果这个阶段他觉得自己要提升实力来维持在工作中的竞争力,那么他就可以选择成长更快的工作 B。</p><p>和张三不同,李四只想攒点钱买个房子,对他来说工作就只是一份工作。但如果他不提升自己的能力,或许就会面临被淘汰、也没法赚到足够的钱,因此李四可能需要在某个阶段选择更具挑战性的工作 B。</p><p>每个人的愿望都不一样,有些人希望攒一些积蓄、买个小房子、过点小日子,也有人希望在职场叱咤风云、留下自己的名字,还有人希望沉浸在自己的世界、一直钻研某个领域。我们的愿望决定了我们最终的目标,而为了实现这个目标,中间也可能会做些看起来与目标相背离的事情。</p><p>我们在做的事情到底有没有用、能不能到达想去的远方,这些只有时间能证明。很多时候,每个人看到的只有当下的片刻,我们不能因为现在眼里的自己不像自己,就感到不甘、难过、想要自暴自弃。同样也不能因为目前眼里其他人的趋利避害、趋于世俗而瞧不起对方。或许有些人我们无法理解也无法接受,但即使认为不是一路人,也该给予他们足够的尊重。</p><h4 id="为什么不可以一边赚钱一边做喜欢的事情呢"><a href="#为什么不可以一边赚钱一边做喜欢的事情呢" class="headerlink" title="为什么不可以一边赚钱一边做喜欢的事情呢"></a>为什么不可以一边赚钱一边做喜欢的事情呢</h4><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>为什么项目复盘很重要</title>
<link href="https://godbasin.github.io/2023/03/21/why-project-reviews-are-important/"/>
<id>https://godbasin.github.io/2023/03/21/why-project-reviews-are-important/</id>
<published>2023-03-21T03:59:35.000Z</published>
<updated>2023-03-21T04:00:09.730Z</updated>
<content type="html"><![CDATA[<p>当开发的个人能力成长到一定程度时,日常工作不再是缝缝补补、修修 bug、打打下手。</p><p>开发时间足够长时,我们常常会以项目的形式参与到具体的开发中,可能会负责项目的主导,或是作为核心开发负责某个模块、某个技术方案的落地。</p><p>在项目进行的每个阶段,我们都可以通过同样的方式去提升自己:</p><ol><li>事前做预期。</li><li>事后做复盘。</li></ol><span id="more"></span><h2 id="事前做预期"><a href="#事前做预期" class="headerlink" title="事前做预期"></a>事前做预期</h2><p>就像在代码开发前进行架构设计一样重要,我们在项目开始前,需要对项目的整个过程进行初步的预期,包括:</p><ol><li>预期功能是否能实现?存在哪些不确定的功能?</li><li>预计的工作量和分工排期是怎样的?</li><li>每个阶段(开发、联调、产品体验、提测、发布)的时间点大概是怎样的?</li><li>哪些工作涉及外部资源的依赖和对接(交互/设计/接口协议等),是否存在延期风险?</li><li>如果存在风险,有没有什么方式可以避免?</li></ol><p>这么做有什么好处呢?如果不做方案调研和项目预期管理,那么对于项目过程中的风险则很难预测。这会导致项目的延期,甚至做到一般发现做不下去了。</p><p>在我们日常的工作中,这样的情况常常会遇到,很多人甚至对需求延期都已经习以为常了,认为需求能准时上线才是稀奇的事情。正因为大家都是这样的想法,我们更应该把这些事情做好来,这样才可以弯道超车。</p><p>首先,在项目开始的时候,需要进行工作量评估和分工排期。</p><h3 id="如何进行合理的分工排期"><a href="#如何进行合理的分工排期" class="headerlink" title="如何进行合理的分工排期"></a>如何进行合理的分工排期</h3><p>进行工作量评估的过程可以分为三步:</p><ol><li>确认技术方案,以及分工合作方式。</li><li>拆分具体功能模块,分别进行工作量评估,输出具体的排期时间表。</li><li>标注资源依赖情况和协作存在的风险,进行延期风险评估。</li></ol><p>当我们确认好技术方案之后,可以针对实现细节拆分具体的功能模块,分别进行工作量的预估和分工排期。具体的分工排期在多人协作的时候是必不可少的,否则可能面临分工不明确、接口协议未对齐就匆忙开工、最终因为各种问题而返工这些问题。</p><p>进行工作量评估的时候,可以精确到半天的工作量预期。对独自开发的项目来说,同样可以通过拆解功能模块这个过程,来思考具体的实现方式,也能提前发现一些可能存在的问题,并相应地进行规避。</p><p>提供完整的工作量评估和排期计划表(精确到具体的日期),可以帮助我们有计划地推进项目。在开发过程中,我们可以及时更新计划的执行情况,团队的其他人也可以了解我们的工作情况。</p><p>工作量评估和排期计划表的另外一个重要作用,是通过时间线去严格约束我们的工作效率、及时发现问题,并在项目结束后可针对时间维度进行项目复盘。</p><p>为了确保项目能按照预期进行,我们还要对可能存在的风险进行分析,提前做好对应的准备措施。</p><h3 id="对项目风险进行把控"><a href="#对项目风险进行把控" class="headerlink" title="对项目风险进行把控"></a>对项目风险进行把控</h3><p>我们在项目开发过程中,经常会遇到这样的情况:</p><ul><li>因为方案设计考虑不周,部分工作需要返工,导致项目延期</li><li>在项目进行过程中,常常会遇到依赖资源无法及时给到、依赖方因为种种原因无法按时支援等问题,导致项目无法按计划进行</li><li>团队协作方式未对齐,开发过程中出现矛盾,反复的争执和调整协作方式导致项目延期</li></ul><p>一个项目能按照预期计划进行,技术方案设计、分工和协作方式、依赖资源是否确定等,任何一各环节出现问题都可能导致整体的计划出现延误,这是我们不想出现的结果。</p><p>因此,我们需要主动把控各个环节的情况,及时推动和解决出现的一些多方协作的问题。</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>性能优化的工作可以用具体的耗时和 CPU 资源占用这些数据来做总结,工具的开发可以用接入使用的用户数量来说明效果,这种普普通通的项目上线,又该怎么表达呢?</p><p>我们可以用两个维度复盘:</p><ol><li>时间维度。</li><li>质量维度。</li></ol><p>其中,时间维度可以包括:</p><ul><li>项目的预期启动、转体验、提测、灰度、全量时间</li><li>项目的最终启动、转体验、提测、灰度、全量时间</li></ul><p>通过预期和最终结果的对比,我们可以直观看到是否存在延期等情况,分析原因分别是什么(比如方案设计问题、人员变动、协作方延期等)</p><p>如下图,假设项目分为一期、二期,我们可以在一期结束后,进行复盘分析并改进。同时还可以以时间线的方式对比开发时间结果:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/my-career5-6.jpg" alt=""></p><p>除了时间维度以外,我们还可以通过衡量项目质量的方式来复盘,比如:</p><ul><li>代码是否有单测、自动化测试保证质量</li><li>产品体验阶段的问题、提测后 BUG 分别有多少</li><li>灰度和全量后的用户反馈有多少</li></ul><p>我们需要分析各个阶段存在的质量问题,并寻找原因(比如技术方案变更时考虑不全、设计稿还原度较低、自测时间不足等)。质量的维度同样可以用对比的方式来展示:</p><p><img src="https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/my-career5-7.jpg" alt=""></p><p>所以,为什么项目复盘很重要呢?</p><ol><li>及时发现自己的问题并改进,避免掉进同一个坑。</li><li>让团队成员和管理者知道自己在做什么。</li><li>整理沉淀和分享项目经验,让整个团队都得到成长。</li></ol><h3 id="输出结果"><a href="#输出结果" class="headerlink" title="输出结果"></a>输出结果</h3><p>很多人会觉得做一个普通的前端项目,从开发到上线都没什么难度。一个字:“干”就完了。</p><p>实际上,项目的管理、推动和落地是工作中不可或缺的能力,这些不同于技术方案设计、代码编写,属于工作中的软技能。但正是这样的软技能会很大地影响我们的工作成果,也会影响自身的成长速度,是升职加薪的必备技能。</p><p>职场之所以让人不适,很多时候是由于它无法做到完美的公平。对于程序员来说,同样如此。</p><p>因此,为了能让自己付出的努力事半功倍,阶段性的输出是必不可少的。对于项目复盘来说,我们可以通过团队内外分享、邮件复盘总结等方式进行输出。</p><p>一般来说,可以通过几个方面来总结整理:</p><ol><li>项目背景,比如为什么启动项目、目标是什么之类。</li><li>技术方案,是否做了技术选型、架构设计等。</li><li>项目结果,时间维度和质量维度,最好有数据佐证。</li><li>未来规划/优化方向。</li></ol><p>通过对项目进行复盘,除了可以让团队其他人和老板知道我们做了些什么,更重要的是,我们可以及时发现自身的一些问题并改进。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>本文介绍了在项目开发过程中,要如何做好前期的准备,又该如何在项目结束后进行完整的复盘。</p><p>对于大部分前端开发来说,接触工具和框架开发、参与开源项目的机会比较少,很多时候我们写的都是“枯燥无聊”的业务代码。我们总认为只有做工具才会比较有意思、有技术挑战,很多时候会先入为主,认为业务代码写得再好也没用,也渐渐放弃了去思考要怎么把事情做好。</p><p>其实不只是工作中,我们生活里也可以常常进行反思和总结,这样我们的步伐才可以越跑越快。成长的过程中总会遇到各式各样的问题,有些问题被我们视而不见,有些问题我们选择了躲开,但其实我们还可以通过迎面应战、解决并反思的方式,在这样一次次战斗中快速地成长。</p>]]></content>
<summary type="html">
<p>当开发的个人能力成长到一定程度时,日常工作不再是缝缝补补、修修 bug、打打下手。</p>
<p>开发时间足够长时,我们常常会以项目的形式参与到具体的开发中,可能会负责项目的主导,或是作为核心开发负责某个模块、某个技术方案的落地。</p>
<p>在项目进行的每个阶段,我们都可以通过同样的方式去提升自己:</p>
<ol>
<li>事前做预期。</li>
<li>事后做复盘。</li>
</ol>
</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/2023/01/12/design-and-manage-front-end-project/"/>
<id>https://godbasin.github.io/2023/01/12/design-and-manage-front-end-project/</id>
<published>2023-01-12T02:38:02.000Z</published>
<updated>2023-01-12T02:38:18.117Z</updated>
<content type="html"><![CDATA[<p>如果说基础知识的掌握是起跑线,那么使大家之间拉开差距的更多是前端项目开发经验和技能。对于一个项目来说,从框架选型和搭建,到项目维护、工程化和自动化、多人协作等各个方面,都需要我们在参与项目中不断地思考和改进,积累经验。</p><span id="more"></span><p>本文将要介绍:</p><ul><li>前端项目设计</li><li>前端项目管理</li></ul><h2 id="前端项目设计"><a href="#前端项目设计" class="headerlink" title="前端项目设计"></a>前端项目设计</h2><p>除了具体的前端领域知识以外,当我们开始负责起整个前端项目的管理时,需要具备一些方案选型、架构设计、项目瓶颈识别并解决等能力。</p><h3 id="前端项目搭建"><a href="#前端项目搭建" class="headerlink" title="前端项目搭建"></a>前端项目搭建</h3><p>很多时候,我们的项目在刚搭建的时候规模会比较小,因此在项目启动阶段需要做简化,来保证项目能快速地上线。但从长期来看,一个项目还需要考虑到拓展性。换句话说,当项目开始变得较难维护的时候,我们就要进行一些架构或者流程上的调整。</p><p>在项目开始之前,我们需要做一系列的规划,像项目的定位(to B/C)、大小,像框架和工具的选型、项目和团队规范等,包括:</p><ul><li>前端框架选择:基于团队成员偏好和能力,选择适合的前端框架</li><li>工具库选择:基于项目规模,选择是否需要路由管理、状态管理等工具库</li><li>自动化工具:基于成员规模和项目状态(快速上线、稳定维护等),选择是否需要代码构建、自动化测试等自动化工具,以及搭建持续集成、持续部署等自动化流程</li><li>项目流程规范:使用一致的项目规范,包括项目代码结构、代码规范、开发流程规范、多人协作规范等内容</li></ul><p>项目的维护永远是程序员的大头,多是“前人种树,后人乘凉”。但是很多时候,大家会为了一时的方便,对代码规范比较随意,就导致了我们经常看到有人讨论“继承来的代码”。</p><p>代码规范其实是团队合作中最重要的地方,使用一致的代码规范,会大大减少协作的时候被戳到的痛点。好的写码习惯很重要,包括友好的变量命名、适当的注释等,都会对代码的可读性有很大的提升。但是习惯是每个人都不一样,所以在此之上,我们需要有这样统一的代码规范。</p><p>一些工具可以很好地协助我们,像 Eslint 这样的工具,加上代码的打包工具、CI/CD 等流程的协助,可以把一些规范强行标准化,达到代码的统一性。还有像 prettier 这样的工具,可以自动在打包的时候帮我们进行代码规范的优化。</p><p>除了这些简单的命名规范、全等、单引双引等代码相关的规范,还有流程规范也一样重要。比如对代码进行 code review,尤其在改动公共库或是公共组件的时候。</p><p>最重要的还是多沟通。沟通是一个团队里必不可少、又很容易出问题的地方,我们要学会沟通和表达自己。</p><h3 id="洞察项目瓶颈"><a href="#洞察项目瓶颈" class="headerlink" title="洞察项目瓶颈"></a>洞察项目瓶颈</h3><p>我们常常会觉得自己做的项目没有什么意思,每天都是重复的工作、繁琐的业务逻辑、糟糕的历史遗留代码,反观其他人都在做有技术、有难度、有挑战性的工作,越会难以喜欢上自己负责的工作。</p><p>实际上,那些会让我们觉得枯燥、反复、杂乱的工作内容,更是可以去改善做好、并能从中获得成长的地方。涉及前端工作的业务,只有极少一部分的业务例如涉及多人协同的在线文档、或是用户量很大的业务如电商、直播、游戏等,这些业务重心可能会稍微倾向前端,更多时候前端真的会处于编写页面、最多就用 Node.js 写写接入层等状况。好的业务可遇不可求,我们在遇到这些业务之前,就什么都不做了吗?</p><p>大多数工作中,对开发的要求都不仅限于实现功能。如果只是编写代码,刚毕业的应届生花几周时间也一样能做到,那么我们的优势在哪里呢?洞察工作中的瓶颈,并有足够的能力去设计方案、排期开发、解决并复盘,这些技能更能突显我们在岗位上的价值和能力。对团队来说,更需要这样能主动发现并解决问题的成员,而不是安排什么就只做什么的螺丝钉。</p><p>一般来说,用户量较大的项目的瓶颈通常会在兼容性、性能优化这些方面;对于一次性的活动页面,挑战点存在于如何高效地完成一次活动页面的开发或者配置,通常会使用配置系统、结合拖拽以及所见即所得等方式来生成页面;对于经常开发各式各样的管理端系统,优化方向则在于怎么通过脚手架快速地生成需要的项目代码、如何快速地发布上线等。我们要做的,就是找到工作中让自己觉得烦躁和不爽的地方,然后去改进优化它们。</p><h3 id="方案调研与选型对比"><a href="#方案调研与选型对比" class="headerlink" title="方案调研与选型对比"></a>方案调研与选型对比</h3><p>找到项目的痛点或是瓶颈后,就需要设计相应的方案去解决它们。而当我们需要投入人力和时间成本去做一件事,就需要面临一个问题:如何让团队认同这件事情、并愿意给到资源让我们去完成它?</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><p>提供完整的工作量评估和时间表,我们可以比较有计划地进行开发,同时团队的其他人也可以了解我们的工作情况,有时候大家能给到一些建议,也能避免对方不了解我们到底在做什么而导致的一些误会。而排期预估的另外一个重要作用,则是通过时间线去严格约束我们的工作效率、及时发现问题,以及项目结束后可针对时间维度进行项目复盘。</p><h3 id="风险把控"><a href="#风险把控" class="headerlink" title="风险把控"></a>风险把控</h3><p>前面有说到,我们需要在参与项目的过程中具备 Owner 意识,即使这个项目并不是我们主导。风险把控则是作为 Owner 必须掌握的一个能力,我们需要确保项目能按照预期进行,则需要主动发现其中可能存在的风险并提前解决。</p><p>除了因为方案设计考虑不周而导致的一些返工风险,我们在项目进行过程中常常也会遇到依赖资源无法及时给到、依赖方因为种种原因无法按时支援、团队协作出现矛盾等各种问题,任何一块出现问题都可能导致整体的工期出现延误,这是我们不想出现的结果。因此,我们需要主动把控各个环节的情况,及时推动和解决出现的一些多方协作的问题。</p><p>通过前期准备的这些方案和工具,提前控制好一些可预见的风险,开发过程会更加顺利。但是如果我们的效果只有这些的话,很多时候是无法证明自己做了这么多事情的价值。那么,我们可以尝试用数据说话。</p><h3 id="及时反馈与复盘"><a href="#及时反馈与复盘" class="headerlink" title="及时反馈与复盘"></a>及时反馈与复盘</h3><p>很多开发习惯了当代码开发完成、发布上线之后就结束了这个项目,其实他们遗漏了一个很重要的环节:复盘。通过复盘这种方式,我们可以发现自身的一些问题并改进,还可以让团队其他人以及管理者知道我们做了些什么,这是很重要的。</p><p>复盘的总结内容,可以通过邮件的方式发送给团队以及合作方,同时还可以作为自身的经验沉淀,后续更多项目中可以进行参考。如果使用得当,我们还可以通过这种方式来影响我们的团队和管理者,也是向上管理的一种方法。</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%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/2022/12/03/research-and-design-process/"/>
<id>https://godbasin.github.io/2022/12/03/research-and-design-process/</id>
<published>2022-12-03T11:18:23.000Z</published>
<updated>2022-12-03T11:19:10.958Z</updated>
<content type="html"><![CDATA[<p>技术方案设计属于架构能力中的一种,当我们开始作为某些功能/应用的 Owner 或是技术负责人来参与项目时,便会面临独立完成技术方案的调研和设计这样的工作内容。</p><span id="more"></span><p>一般来说,技术方案的调研和设计过程可以分为几个阶段:</p><ol><li>对项目的痛点、现状进行分析。</li><li>调用业界成熟的技术方案。</li><li>结合项目本身的现状和痛点,进行技术方案的选型和对比。</li></ol><h2 id="技术方案调研"><a href="#技术方案调研" class="headerlink" title="技术方案调研"></a>技术方案调研</h2><p>只有确保了技术方案的最优化、避免开发过程遇到问题需要推翻重做,从而能够快速落地并达成预期的效果。因此,在进行方案设计之前,对于项目存在的一些技术瓶颈、技术调整,我们需要先进行充分的前期调研。</p><p>在进行技术方案调研的时候,我们需要首先结合自身项目的背景、存在的痛点、现状问题进行分析,只有找到项目的问题在哪里,才可以更准确、彻底地去解决这些问题。</p><h3 id="分析项目背景,挖掘项目痛点"><a href="#分析项目背景,挖掘项目痛点" class="headerlink" title="分析项目背景,挖掘项目痛点"></a>分析项目背景,挖掘项目痛点</h3><p>技术方案的设计很多时候并不是命题作文,更多时候我们需要自己去挖掘项目的痛点,然后才是提出解决方案。</p><p>很多前端开发常常觉得自己做的项目没什么意思,认为每天都是重复的工作、繁琐的业务逻辑、糟糕的历史遗留代码。</p><p>实际上,那些会让我们觉得枯燥和重复的工作内容,也是可以去改善做好、并能从中获得成长的地方。好的业务可遇不可求,如果工作内容跟自己的预期不一样,我们就什么都不做了吗?</p><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>多人协作容易出问题 -> 提升系统稳定性</li></ul><p>确定目标之后,我们就需要进行技术方案的设计,但很多时候由于项目现状存在的问题,一些技术优化的方案并不适用,需要进行方向的调整。</p><p>假设有一个同样规模大、成员多的小程序项目,由于该项目处于快速迭代的时期,考虑到投入产出比、产品形态也在不断调整,老板说“每个功能由开发自己保证”,决定不投入测试资源。</p><p>这意味着开发不仅需要在自测的时候确保核心用例的覆盖,同时也没有足够的排期来进行自动化测试(单元测试、集成测试、端到端测试等)的开发。</p><p>一般来说,我们还可以考虑建立用例录制和自动化回归的解决方案。比如开发一个浏览器插件,来获取用户操作的一些行为(比如 Redux 中的 Action 操作),将操作行为的页面结果(状态数据,比如 Redux 的 State)保存下来。在发布之前,可以通过自动化触发相同的操作行为,并与录制的页面结果进行比较,来进行回归测试。</p><p>但对于小程序的特殊性,我们无法让其运行在浏览器中,更无法获取到它的操作行为。在这样的情况下,还有什么办法可以保证系统的稳定性呢?</p><p>考虑到一个系统的上线过程包括开发、测试、灰度和发布四个阶段,如果无法通过测试阶段来及时发现问题,那么我们还可以通过灰度过程中来及时发现并解决问题。</p><p>比如,通过全埋点覆盖各个页面的功能,灰度过程中观察埋点曲线是否有异常、及时告警和排查问题、暂停灰度或者回滚等方式,来避免给更多的用户带来不好的体验。</p><p>通过灰度的方式来保证系统稳定性,会对局部的用户造成影响,这并不是一个最优的技术方案,它是考虑到项目的现状退而求其次的解决方案,但最终也同样可以达到提升系统稳定性这样一个目的。</p><p>当我们确定了技术优化的具体方向之后,便可以进行业界方案的调研阶段了。</p><h3 id="业界方案调研"><a href="#业界方案调研" class="headerlink" title="业界方案调研"></a>业界方案调研</h3><p>当我们遇到一些技术问题并尝试解决的时候,需要提醒自己,这些问题肯定有其他人也遇到过。为了避免技术方案的设计过于局限,我们可以进行前期的调研,找一些业界相对成熟的方案作为参考,分析这些方案的优缺点、是否适用于自己的项目中。</p><p>我们可以通过几种方式去进行业界方案的调研:</p><ol><li>与有相关经验的开发进行沟通,交流技术方案,提供参考思路。</li><li>参考其他系统对外公开的方案设计。</li><li>参考开源项目的源码设计。</li></ol><p>举个例子,对于交互复杂、规模大型的应用,要如何管理各个模块间的依赖关系呢?业界相对成熟的解决方案是使用依赖注入体系,其中著名的开源项目中有 Angular 和 VsCode 都实现了依赖注入的框架,我们可以通过研究它们的相关代码,分析其中的思路以及实现方式。</p><p>开源项目源码很多,要怎么才能找到自己想看的部分呢?带着疑问有目的性地看,会简单轻松得多。比如上述的依赖注入框架,我们可以带着以下的问题进行研究:</p><ol><li>依赖注入框架是什么?</li><li>模块是怎样初始化,什么时候进行销毁的?</li><li>模块是如何获取到其它模块呢?</li><li>模块间是如何进行通信的呢?</li></ol><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><p>例如,假设我们最近需要针对项目进行自动化性能测试能力的支持:</p><ul><li>项目现状:项目规模大、模块多、参与开发的成员也有几十人</li><li>项目痛点:经常因为一些不同模块的变更导致项目的性能下降却没法及时发现问题,往往是等到用户反馈或是某次开发、产品或者测试发现的时候才得知</li></ul><p>调研常见的一些性能分析方案,发现有几种方式:</p><ol><li>通过 Chrome Devtools 提供的 Performace 火焰图,来定位和发现问题,但这种方式局限于开发手动分析定位。</li><li>使用 Lighthouse,该工具可以提供初步的网页优化建议,也支持自动化。但 Lighthouse 本身更专注于短时间内对网站进行较全面的评估,存在像分析不够细致和深入这些问题。</li><li>使用 Chrome Devtools 提供的 Chrome Devtools Protocol(CDP)能力,进行自动化生成火焰图需要的 JSON。但业界对该 JSON 的分析工具几乎没有,大家都通过将该 JSON 传到 Chrome Devtools 提供的一个工具来还原火焰图,无法支持全程的自动化分析。</li></ol><p>其中,第一和第二种方案都无法从根本上解决遇到的问题。如果要彻底解决这个问题,可以考虑采取第三种方案,并打算通过自行研究分析 CDP(Chrome Devtools Protocol)生成的 JSON 来达到完全的自动化目的。</p><p>方案选型和对比是技术方案设计中重要的一个环节,可以将现状和痛点分析得更加全面,同时还可以避开一些其他人踩过的坑。</p><h2 id="结束语"><a href="#结束语" class="headerlink" title="结束语"></a>结束语</h2><p>在大多数工作中,对开发的要求都不仅限于实现功能。你是否有想过,如果只是编写代码,刚毕业的应届生花几周时间也一样能做到,那么我们的优势在哪里呢?</p><p>洞察工作中的瓶颈,并有足够的能力去设计方案、排期开发、解决并复盘,这些技能更能突显我们在岗位上的价值和能力。对团队来说,更需要这样能主动发现并解决问题的成员,而不是安排什么就只做什么的螺丝钉。</p><p>技术的发展都离不开知识的沉淀、分享和相互学习,当我们遇到一些问题不知道该怎么解决的时候,可以试着站到巨人的肩膀上,说不定可以看到更多。</p>]]></content>
<summary type="html">
<p>技术方案设计属于架构能力中的一种,当我们开始作为某些功能/应用的 Owner 或是技术负责人来参与项目时,便会面临独立完成技术方案的调研和设计这样的工作内容。</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>
</feed>