<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          代碼層面探索前端性能

          共 21380字,需瀏覽 43分鐘

           ·

          2023-10-16 08:43



          一、前言

          最近在做性能優(yōu)化,具體優(yōu)化手段,網(wǎng)上鋪天蓋地,這里就不重復(fù)了。

          性能優(yōu)化可分為以下幾個維度:代碼層面、構(gòu)建層面、網(wǎng)絡(luò)層面。
          本文主要是從代碼層面探索前端性能,主要分為以下 4 個小節(jié)。
          • 使用 CSS 替代 JS
          • 深度剖析 JS
          • 前端算法
          • 計算機(jī)底層


          二、使用 CSS 替代 JSs
          這里主要從動畫和 CSS 組件兩個方面介紹。

          CSS 動畫

          CSS2 出來之前,哪怕要實現(xiàn)一個很簡單的動畫,都要通過 JS 實現(xiàn)。比如下面紅色方塊的水平移動:
          對應(yīng) JS 代碼:
             
             
          let redBox = document.getElementById('redBox')let l = 10
          setInterval(() => { l+=3 redBox.style.left = `${l}px`}, 50)
          1998 年的 CSS2 規(guī)范,定義了一些動畫屬性,但由于受當(dāng)時瀏覽器技術(shù)限制,這些特性并沒有得到廣泛的支持和應(yīng)用。
          直到 CSS3 的推出,CSS 動畫得到了更全面地支持。同時,CSS3 還引入了更多的動畫效果,使得 CSS 動畫在今天的 Web 開發(fā)中得到了廣泛的應(yīng)用。
          那么 CSS3 都能實現(xiàn)什么動畫,舉幾個例子:
          • 過渡(Transition) - 過渡是 CSS3 中常用的動畫效果之一,通過對一個元素的某些屬性進(jìn)行變換,使元素在一段時間內(nèi)從一個狀態(tài)平滑地過渡到另一個狀態(tài)。
          • 動畫(Animation) - 動畫是 CSS3 中另一個常用的動畫效果,其用于為一個元素添加一些復(fù)雜的動畫效果,可以通過關(guān)鍵幀(@keyframes)來定義一串動畫序列。
          • 變換(Transform) - 變換是 CSS3 中用于實現(xiàn) 2D/3D 圖形變換效果的一種技術(shù),包括旋轉(zhuǎn)、縮放、移動、斜切等效果。
          把上面的例子改寫成 CSS 代碼如下:
             
             
          #redBox {    animation: mymove 5s infinite;}
          @keyframes mymove{ from {left: 0;} to {left: 200px;}}
          同樣的效果,用樣式就能實現(xiàn),何樂而不為呢。
          需要指出的是,CSS 的動畫仍在不斷發(fā)展和改進(jìn),隨著新的瀏覽器特性和 CSS 版本的出現(xiàn),CSS 動畫的特性也在不斷地增加和優(yōu)化,以滿足日益復(fù)雜的動畫需求和更好的用戶體驗。

          CSS 組件

          在一些知名的組件庫中,有些組件的大部分 props 是通過修改 CSS 樣式實現(xiàn)的,比如 Vant 的 Space 組件。

          Props
          功能
          CSS樣式
          direction
          間距方向
          flex-direction: column;
          align
          對齊方式
          align-items: xxx;
          fill
          是否讓 Space 變?yōu)橐粋€塊級元素,填充整個父元素
          display: flex;
          wrap
          是否自動換行
          flex-wrap: wrap;

          再比如 Ant Design 的 Space 組件。

          Props

          功能

          CSS樣式

          align

          對齊方式

          align-items: xxx;

          direction

          間距方向

          flex-direction: column;

          size

          間距大小

          gap: xxx;

          wrap

          是否自動換行

          flex-wrap: wrap;

          這類組件完全可以封裝成 SCSS 的 mixin 實現(xiàn)(LESS 也一樣),既能減少項目的構(gòu)建體積(兩個庫的 Space 組件 gzip 后的大小分別為 5.4k 和 22.9k),又能提高性能。

          查看組件庫某個組件的體積,可訪問連接:https://bundlephobia.com/。

          比如下面的 space mixin:

             
             
          /* * 間距* size: 間距大小,默認(rèn)是 8px* align: 對齊方式,默認(rèn)是 center,可選 start、end、baseline、center* direction: 間距方向,默認(rèn)是 horizontal,可選 horizontal、vertical* wrap: 是否自動換行,僅在 horizontal 時有效,默認(rèn)是 false*/@mixin space($size: 8px, $direction: horizontal, $align: center, $wrap: false) {    display: inline-flex;    gap: $size;
          @if ($direction == 'vertical') { flex-direction: column; }
          @if ($align == 'center') { align-items: center; }
          @if ($align == 'start') { align-items: flex-start; }
          @if ($align == 'end') { align-items: flex-end; }
          @if ($align == 'baseline') { align-items: baseline; }
          @if ($wrap == true) { @if $direction == 'horizontal' { flex-wrap: wrap; } }}
          類似的組件還有 Grid、Layout 等。
          再說下圖標(biāo),下面是 Ant Design 圖標(biāo)組件的第一屏截圖,有很多僅用 HTML + CSS 就可以輕松實現(xiàn)。
          實現(xiàn)思路:
          • 優(yōu)先考慮只使用樣式實現(xiàn)
          • 僅靠樣式滿足不了,就先增加一個標(biāo)簽,通過這個標(biāo)簽和它的兩個偽元素 ::before 和 ::after 實現(xiàn)
          • 一個標(biāo)簽實在不夠,再考慮增加額外的標(biāo)簽
          比如實現(xiàn)一個支持四個方向的實心三角形,僅用幾行樣式就可以實現(xiàn)(上面截圖是 4 個圖標(biāo)):
             
             
          /* 三角形 */@mixin triangle($borderWidth: 10, $shapeColor: #666, $direction: up) {    width: 0;    height: 0;    border: if(type-of($borderWidth) == 'number', #{$borderWidth} + 'px', #{$borderWidth}) solid transparent;
          $doubleBorderWidth: 2 * $borderWidth; $borderStyle: if(type-of($doubleBorderWidth) == 'number', #{$doubleBorderWidth} + 'px', #{$doubleBorderWidth}) solid #{$shapeColor};
          @if($direction == 'up') { border-bottom: $borderStyle; }
          @if($direction == 'down') { border-top: $borderStyle; }
          @if($direction == 'left') { border-right: $borderStyle; }
          @if($direction == 'right') { border-left: $borderStyle; }}
          總之,能用 CSS 實現(xiàn)的就不用 JS,不僅性能好,而且還跨技術(shù)棧,甚至跨端。


          三、深度剖析JS

          介紹完了 CSS,再來看 JS,主要從基本語句和框架源碼兩個方面深入。

          if-else 語句的優(yōu)化

          先了解下 CPU 是如何執(zhí)行條件語句的。參考如下代碼:
             
             
          const a = 2const b = 10let cif (a > 3) {    c = a + b} else {    c = 2 * a}
          CPU 執(zhí)行流程如下:
          我們看到,在執(zhí)行到指令 0102 時候,由于不滿足 a > 3 這個條件,就直接跳轉(zhuǎn)到 0104 這個指令去執(zhí)行了;而且,計算機(jī)很聰明,如果它在編譯期間發(fā)現(xiàn) a 永遠(yuǎn)不可能大于 3,它就會直接刪除 0103 這條指令,然后,0104 這條指令就變成了下一條指令,直接順序執(zhí)行,也就是編譯器的優(yōu)化。
          那么回到正題,假如有以下代碼:
             
             
          function check(age, sex) {    let msg = ''    if (age > 18) {        if (sex === 1) {            msg = '符合條件'        } else {            msg = ' 不符合條件'        }    } else {        msg = '不符合條件'    }}
          邏輯很簡單,就是篩選出 age > 18 并且 sex == 1 的人,代碼一點兒問題都沒有,但是太啰嗦,站在 CPU 的角度來看,需要執(zhí)行兩次跳轉(zhuǎn)操作,當(dāng) age > 18 時,就進(jìn)入內(nèi)層的 if-else 繼續(xù)判斷,也就意味著再次跳轉(zhuǎn)。
          其實我們可以直接優(yōu)化下這個邏輯(通常我們也是這樣做的,但是可能知其然而不知其所以然):
             
             
          function check(age, sex){    if (age > 18 && sex ==1) return '符合條件'    return '不符合條件'}
          所以,邏輯能提前結(jié)束就提前結(jié)束,減少 CPU 的跳轉(zhuǎn)。

          Switch 語句的優(yōu)化

          其實 switch 語句和 if-else 語句的區(qū)別不大,只不過寫法不同而已,但是,switch 語句有個特殊的優(yōu)化,那就是數(shù)組。

          參考以下代碼:
             
             
          function getPrice(level) {    if (level > 10) return 100    if (level > 9) return 80    if (level > 6) return 50    if (level > 1) return 20    return 10}
          我們改成 switch 語句:
             
             
          function getPrice(level) {    switch(level)        case 10: return 100        case 9: return 80        case 8:         case 7:         case 6: return 50        case 5:        case 4:         case 3:        case 2:         case 1: return 20        default: return 10}
          看著沒啥區(qū)別,其實編譯器會把它優(yōu)化成一個數(shù)組,其中數(shù)組的下標(biāo)為 0 到 10,不同下標(biāo)對應(yīng)的價格就是 return 的數(shù)值,也就是:
          而我們又知道,數(shù)組是支持隨機(jī)訪問的,速度極快,所以,編譯器對 switch 的這個優(yōu)化就會大大提升程序的運行效率,這可比一條一條執(zhí)行命令快多了。
          那么,我還寫什么 if-else 語句啊,我直接全部寫 switch 不就行了?
          不行!因為編譯器對 switch 的優(yōu)化是有條件的,它要求你的 code 必須是緊湊的,也就是連續(xù)的。
          這是為什么呢?因為我要用數(shù)組來優(yōu)化你啊,你如果不是緊湊的,比如你的 code 是 1、50、51、101、110,我就要創(chuàng)建一個長度 110 的數(shù)組來存放你,只有這幾個位置有用,豈不是浪費空間!
          所以,我們在使用 switch 的時候,盡量保證 code 是緊湊的數(shù)字類型的。

          循環(huán)語句的優(yōu)化

          其實循環(huán)語句跟條件語句類似,只不過寫法不同而已,循環(huán)語句的優(yōu)化點是以減少指令為主。
          我們先來看一個中二的寫法:
             
             
          function findUserByName(users) {   let user = null   for (let i = 0; i < users.length; i++) {       if (users[i].name === '張三') {           user = users[i]       }   }   return user}
          如果數(shù)組長度是 10086,第一個人就叫張三,那后面 10085 次遍歷不就白做了,真拿 CPU 不當(dāng)人啊。
          你直接這樣寫不就行了:
             
             
          function findUserByName(users) {    for (let i = 0; i < users.length; i++) {        if (users[i].name === '章三') return users[i]    }}
          這樣寫效率高,可讀性強(qiáng),也符合我們上述的邏輯能提前結(jié)束就提前結(jié)束這個觀點。CPU 直接感謝你全家。
          其實,這里還有一點可以優(yōu)化的地方,就是我們的數(shù)組長度可以提取出來,不必每次都訪問,也就是這樣:
             
             
          function findUserByName(users) {    let length = users.length    for (let i = 0; i < length; i++) {        if (users[i].name === '章三') return users[i]    }}
          這看起來好像有點吹毛求疵了,確實是,但是如果考慮到性能的話,還是有點用的。比如有的集合的 size() 函數(shù),不是簡單的屬性訪問,而是每次都需要計算一次,這種場景就是一次很大的優(yōu)化了,因為省了很多次函數(shù)調(diào)用的過程,也就是省了很多個 call 和 return 指令,這無疑是提高了代碼的效率的。尤其是在循環(huán)語句這種容易量變引起質(zhì)變的情況下,差距就是從這個細(xì)節(jié)拉開的。
          函數(shù)調(diào)用過程參考:
          對應(yīng)代碼如下:
             
             
          let a = 10let b = 11
          function sum (a, b) { return a + b}
          說完了幾個基礎(chǔ)語句,再來看下我們經(jīng)常使用的框架內(nèi)部,很多地方的性能都值得探索。

          diff 算法

          Vue 和 React 中都使用了虛擬 DOM,當(dāng)執(zhí)行更新時,要對比新舊虛擬 DOM。如果沒有任何優(yōu)化,直接嚴(yán)格 diff 兩棵樹,時間復(fù)雜度是 O(n^3),根本不可用。所以 Vue 和 React 必須使用 diff 算法優(yōu)化虛擬 DOM:
          Vue2 - 雙端比較:
          類似上面的圖:
          • 定義 4 個變量,分別為:oldStartIdx、oldEndIdx、newStartIdx 和 newEndIdx
          • 判斷 oldStartIdx 和 newStartIdx 是否相等
          • 判斷 oldEndIdx 和 newEndIdx 是否相等
          • 判斷 oldStartIdx 和 newEndIdx 是否相等
          • 判斷 oldEndIdx 和 newStartIdx 是否相等
          • 同時 oldStartIdx 和 newStartIdx 向右移動;oldEndIdx 和 newEndIdx 向左移動
          Vue3 - 最長遞增子序列:
          整個過程是基于 Vue2 的雙端比較再次進(jìn)行優(yōu)化。比如上面這個截圖:
          • 先進(jìn)行雙端比較,發(fā)現(xiàn)前面兩個節(jié)點(A 和 B)和最后一個節(jié)點(G)是一樣的,不需要移動
          • 找到最長遞增子序列 C、D、E(新舊 children 都包含的,最長的順序沒有發(fā)生變化的一組節(jié)點)
          • 把子序列當(dāng)成一個整體,內(nèi)部不用進(jìn)行任何操作,只需要把 F 移動到它的前面,H 插入到它的后面即可
          React - 僅右移:
          上面截圖的比較過程如下:
          • 遍歷 Old 存下對應(yīng)下標(biāo) Map
          • 遍歷 New,b 的下標(biāo)從 1 變成了 0,不動(是左移不是右移)
          • c 的下標(biāo)從 2 變成了 1,不動(也是左移不是右移)
          • a 的下標(biāo)從 0 變成了 2,向右移動,b、c 下標(biāo)都減 1
          • d 和 e 位置沒變,不需要移動
          總之,不管用什么算法,它們的原則都是:
          • 只比較同一層級,不跨級比較
          • Tag 不同則刪掉重建(不再去比較內(nèi)部的細(xì)節(jié))
          • 子節(jié)點通過 key 區(qū)分(key 的重要性)
          最后也都成功把時間復(fù)雜度降低到了 O(n),才可以被我們實際項目使用。

          setState 真的是異步嗎

          很多人都認(rèn)為 setState 是異步的,但是請看下面的例子:
             
             
          clickHandler = () => {    console.log('--- start ---')
          Promise.resolve().then(() => console.log('promise then'))
          this.setState({val: 1}, () => {console.log('state...', this.state.val)})
          console.log('--- end ---')}
          render() { return <div onClick={this.clickHandler}>setState</div>}
          實際打印結(jié)果:
          如果是異步的話,state 的打印應(yīng)該在微任務(wù) Promise 后執(zhí)行。
          為了解釋清這個原因,必須先了解 JSX 里的事件機(jī)制。

          JSX 里的事件,比如 onClick={() => {}},其實叫合成事件,區(qū)別于我們常說的自定義事件:
             
             
          // 自定義事件document.getElementById('app').addEventListener('click', () => {})
          合成事件都是綁定在 root 根節(jié)點上,有前置和后置操作,拿上面的例子舉例:
             
             
          function fn() { // fn 是合成事件函數(shù),內(nèi)部事件同步執(zhí)行    // 前置    clickHandler()        // 后置,執(zhí)行 setState 的 callback}
          可以想象有函數(shù) fn,里面的事件都是同步執(zhí)行的,包括 setState。fn 執(zhí)行完,才開始執(zhí)行異步事件,即 Promise.then,符合打印的結(jié)果。
          那么 React 為什么要這么做呢?
          因為要考慮性能,如果要多次修改 state,React 會先合并這些修改,合并完只進(jìn)行一次 DOM 渲染,避免每次修改完都渲染 DOM。
          所以 setState 本質(zhì)是同步,日常說的“異步”是不嚴(yán)謹(jǐn)?shù)摹?/span>


          四、前端算法

          講完了我們的日常開發(fā),再來說說算法在前端中的應(yīng)用。

          友情提示:算法一般都是針對大數(shù)據(jù)量而言,區(qū)別于日常開發(fā)。

          能用值類型就不用引用類型

          先來看一道題。
          求 1-10000 之間的所有對稱數(shù),例如:0, 1, 2, 11, 22, 101, 232, 1221...
          思路 1 - 使用數(shù)組反轉(zhuǎn)、比較:數(shù)字轉(zhuǎn)換為字符串,再轉(zhuǎn)換為數(shù)組;數(shù)組 reverse,再 join 為字符串;前后字符串進(jìn)行對比。
             
             
          function findPalindromeNumbers1(max) {    const res = []    if (max <= 0) return res
          for (let i = 1; i <= max; i++) { // 轉(zhuǎn)換為字符串,轉(zhuǎn)換為數(shù)組,再反轉(zhuǎn),比較 const s = i.toString() if (s === s.split('').reverse().join('')) { res.push(i) } }
          return res}
          思路 2 - 字符串頭尾比較:數(shù)字轉(zhuǎn)換為字符串;字符串頭尾字符比較。
             
             
          function findPalindromeNumbers2(max) {    const res = []    if (max <= 0) return res
          for (let i = 1; i <= max; i++) { const s = i.toString() const length = s.length
          // 字符串頭尾比較 let flag = true let startIndex = 0 // 字符串開始 let endIndex = length - 1 // 字符串結(jié)束 while (startIndex < endIndex) { if (s[startIndex] !== s[endIndex]) { flag = false break } else { // 繼續(xù)比較 startIndex++ endIndex-- } }
          if (flag) res.push(res) }
          return res}
          思路 3 - 生成翻轉(zhuǎn)數(shù):使用 % 和 Math.floor 生成翻轉(zhuǎn)數(shù);前后數(shù)字進(jìn)行對比(全程操作數(shù)字,沒有字符串類型)。
             
             
          function findPalindromeNumbers3(max) {    const res = []    if (max <= 0) return res
          for (let i = 1; i <= max; i++) { let n = i let rev = 0 // 存儲翻轉(zhuǎn)數(shù)
          // 生成翻轉(zhuǎn)數(shù) while (n > 0) { rev = rev * 10 + n % 10 n = Math.floor(n / 10) }
          if (i === rev) res.push(i) }
          return res}
          性能分析:越來越快
          • 思路 1- 看似是 O(n),但數(shù)組轉(zhuǎn)換、操作都需要時間,所以慢

          • 思路 2 VS 思路3 - 操作數(shù)字更快(電腦原型就是計算器)

          總之,盡量不要轉(zhuǎn)換數(shù)據(jù)結(jié)構(gòu),尤其數(shù)組這種有序結(jié)構(gòu),盡量不要用內(nèi)置 API,如 reverse,不好識別復(fù)雜度,數(shù)字操作最快,其次是字符串。

          盡量用“低級”代碼

          還是直接上一道題。
          輸入一個字符串,切換其中字母的大小寫
          如,輸入字符串 12aBc34,輸出字符串 12AbC34
          思路 1 - 使用正則表達(dá)式。
             
             
          function switchLetterCase(s) {    let res = ''
          const length = s.length if (length === 0) return res
          const reg1 = /[a-z] const reg2 = /[A-Z]
          for (let i = 0; i < length; i++) { const c = s[i] if (reg1.test(c)) { res += c.toUpperCase() } else if (reg2.test(c)) { res += c.toLowerCase() } else { res += c } }
          return res}
          思路 2 - 通過 ASCII 碼判斷。
             
             
          function switchLetterCase2(s) {    let res = ''
          const length = s.length if (length === 0) return res
          for (let i = 0; i < length; i++) { const c = s[i] const code = c.charCodeAt(0)
          if (code >= 65 && code <= 90) { res += c.toLowerCase() } else if (code >= 97 && code <= 122) { res += c.toUpperCase() } else { res += c } }
          return res}
          性能分析:前者使用了正則,慢于后者
          所以,盡量用“低級”代碼,慎用語法糖、高級 API 或者正則表達(dá)式。


          五、計算機(jī)底層

          最后說一些前端需要了解的計算機(jī)底層。

          從“內(nèi)存”讀數(shù)據(jù)

          我們通常說的:從內(nèi)存中讀數(shù)據(jù),就是把數(shù)據(jù)讀入寄存器中,但是我們的數(shù)據(jù)不是直接從內(nèi)存讀入寄存器的,而是先讀入一個高速緩存中,然后才讀入寄存器的。

          寄存器是在 CPU 內(nèi)的,也是 CPU 的一部分,所以 CPU 從寄存器讀寫數(shù)據(jù)非常快。

          這是為啥呢?因為從內(nèi)存中讀數(shù)據(jù)太慢了。
          你可以這么理解:CPU 先把數(shù)據(jù)讀入高速緩存中,以備使用,真正使用的時候,就從高速緩存中讀入寄存器;當(dāng)寄存器使用完畢后,就把數(shù)據(jù)寫回到高速緩存中,然后高速緩存再在合適的時機(jī)將數(shù)據(jù)寫入到存儲器。
          CPU 運算速度非常快,而從內(nèi)存讀數(shù)據(jù)非常慢,如果每次都從內(nèi)存中讀寫數(shù)據(jù),那么勢必會拖累 CPU 的運算速度,可能執(zhí)行 100s,有 99s 都在讀取數(shù)據(jù)。為了解決這個問題,我們就在 CPU 和存儲器之間放了個高速緩存,而 CPU 和高速緩存之間的讀寫速度是很快的,CPU 只管和高速緩存互相讀寫數(shù)據(jù),而不管高速緩存和存儲器之間是怎么同步數(shù)據(jù)的。這樣就解決了內(nèi)存讀寫慢的問題。

          二進(jìn)制的位運算

          靈活運用二進(jìn)制的位運算不僅能提高速度,熟練使用二進(jìn)制還能節(jié)省內(nèi)存。
          假如給定一個數(shù) n,怎么判斷 n 是不是 2 的 n 次方呢?
          很簡單啊,直接求余就行了。
             
             
          function isPowerOfTwo(n) {    if (n <= 0) return false    let temp = n    while (temp > 1) {        if (temp % 2 != 0) return false        temp /= 2    }    return true}
          嗯,代碼沒毛病,不過不夠好,看下面代碼:
             
             
          function isPowerOfTwo(n) {    return (n > 0) && ((n & (n - 1)) == 0)}
          大家可以用 console.time 和 console.timeEnd 對比下運行速度便知。
          我們可能還會看到一些源碼里面有很多 flag 變量,對這些 flag 進(jìn)行按位與或按位或運算來檢測標(biāo)記,從而判斷是否開啟了某個功能。他為什么不直接用布爾值呢?很簡單,這樣效率高還節(jié)省內(nèi)存。
          比如 Vue3 源碼中的這段代碼,不僅用到了按位與和按位或,還用到了左移:
             
             
          export const enum ShapeFlags {  ELEMENT = 1,  FUNCTIONAL_COMPONENT = 1 << 1,  STATEFUL_COMPONENT = 1 << 2,  TEXT_CHILDREN = 1 << 3,  ARRAY_CHILDREN = 1 << 4,  SLOTS_CHILDREN = 1 << 5,  TELEPORT = 1 << 6,  SUSPENSE = 1 << 7,  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,  COMPONENT_KEPT_ALIVE = 1 << 9,  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT}

          if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) { ...}

          if (hasDynamicKeys) { patchFlag |= PatchFlags.FULL_PROPS } else { if (hasClassBinding) { patchFlag |= PatchFlags.CLASS } if (hasStyleBinding) { patchFlag |= PatchFlags.STYLE } if (dynamicPropNames.length) { patchFlag |= PatchFlags.PROPS } if (hasHydrationEventBinding) { patchFlag |= PatchFlags.HYDRATE_EVENTS }}


          六、結(jié)語

          文章從代碼層面講解了前端的性能,有深度維度的:
          • JS 基礎(chǔ)知識深度剖析
          • 框架源碼
          也有廣度維度的:
          • CSS 動畫、組件
          • 算法
          • 計算機(jī)底層
          希望能讓大家拓寬前端性能的視野,如果對文章感興趣,歡迎留言討論
          -end-

          瀏覽 1650
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄页网站在线观看视频 | 999精品在线视频 | 成人在线视频网站 | 亚洲黄片在线播放 | 91社视频|