當聊到前端性能優化時,咱們會關注什麼?

關於這期分享內容

性能優化一直是前端領域老生常談的問題,系統的性能以及穩定性很大程度上決定着產品的用戶體驗以及產品所能達到的高度。而tob和toc系統又有着不一樣的業務場景,性能優化也有着不用的着力點。本文從筆者的視角出發,結合本身針對一個tob系統的性能優化實踐去剖析一些你們可能共同關注的點,爭取能夠以小見大。html

關於團隊定位

我所在的團隊是一個涉及業務比較複雜的的教育前端團隊,而談及在線教育,始終繞不開在線講義,在線課件這一關,咱們所負責的業務旨在提供完善的在線課件解決方案:前端

咱們輸出的產品主要包括 編輯器渲染器 兩部分。vue

  • 編輯器 除了提供基礎的課件編輯製做能力外,還提供了組裝各種教育資源的能力,這些教育資源包括互動題、cocos、pdf、ppt等。
  • 渲染器 除了提供通用渲染器來支持基礎課件的渲染之外,還支持接入各種教育資源的渲染器,來支持教育資源的渲染。

關於數據結構,大體數據結構以下所示,相似ppt的數據結構,每一頁單頁課件是一個page,每頁課件上中的文字圖片音頻視頻都是一個節點,這些課件頁以及節點都是以數組的形式來維護。node

image.png

{

    pages: [

        data: {

            nodes: [

                'text', 'image', 'video', 'staticQuestion'...

            ]

        }

    ]

}
複製代碼

簡單瞭解業務以後咱們才能結合具體的場景討論性能優化過程當中遇到的問題。react

性能優化歷程

  1. 3-4 雙月立項

咱們的項目規劃通常按照雙月來制定目標,34雙月咱們成立課件性能優化專項,雙月目標是明顯提高用戶體驗。git

  1. 針對不一樣問題的解決方案

下面我會從遇到的具體case入手,來聊一聊咱們是如何解決這些問題的。github

  1. 課件列表頁卡頓

    1. 緣由分析

咱們課件系統的數據依然採用了序列化數據存儲(未分頁),而咱們打開編輯器時,會發請求拿到課件的全部內容,課件內容也會一古腦兒渲染在頁面上,這樣帶來的結果就是頁面的性能很是受課件體量的制約,隨着課件內容愈來愈多,課件頁面達到100頁以上時,系統的性能就已經到達了瓶頸,具體表現爲點擊切換課件頁卡頓以及列表頁滾動卡頓。web

咱們在列表的vue組件的updated 生命週期中添加了一個 log 查看組件渲染次數:vuex

updated() {

    // 查看該組件更新了多少次,勿刪

    console.log("%c left viewer rerender", 'color: red;');

},
複製代碼

Vue 的 updated官網這樣解釋道:api

因爲數據更改致使的虛擬 DOM 從新渲染和打補丁。當這個鉤子被調用時,組件 DOM 已經更新,因此你如今能夠執行依賴於 DOM 的操做。然而在大多數狀況下,你應該避免在此期間更改狀態。若是要相應狀態改變,一般最好使用 計算屬性 watcher 取而代之。

cn.vuejs.org/v2/api/#upd…

因而咱們發現點擊整個單個課件頁時,整個左側列表都從新渲染,而每一個課件頁中的log也會執行,並且會渲染三次。

咱們初步判斷當點擊單頁時,組件執行了多餘的render,而在從新渲染以前虛擬dom的計算阻塞了單線程,致使ui假死。雖然Vue內部對虛擬dom的計算作了不少優化,可是在這個案例中咱們看到,課件體量大時,單線程依然會阻塞,咱們經過performance能夠進一步證實咱們的猜測。

經過 perfermance能夠看到,一次點擊事件的處理時間達到4.16s,這一楨的時間是4500ms,在這四秒多時間內,瀏覽器是沒有任何響應的,而經過觀察咱們發現這段時間耗時的操做就是Vue的虛擬dom計算過程,在Bottom-up中也能夠看到,耗時操做vue removeSub移除依賴的操做,還有虛擬dom patch node 的計算,這個過程是爲了合併更新,這個計算堆積起來就很是耗時。

排查到這裏我將緣由歸結爲組件太多,沒必要要的更新太多,咱們去查看了一下頁面節點數量

200頁的課件所有渲染,頁面節點已經到達了3w之多,而每次交互更新量巨大,瀏覽器重繪的壓力也比較大(雖然這個時間比js計算仍是少不少)。

通過以上的排查,咱們總結緣由爲:Vue的數據偵聽應該更新變化了的dom,可是咱們點擊某個課件頁時,因爲處理Vuex的數據流的方式不太合理,使得許多組件依賴了本不須要的數據,致使Vue判斷組件須要從新渲染。其次咱們的頁面結構過於複雜,沒有作動態渲染或者feed流相似的分片加載策略,浪費了不少資源。

  1. 解決方案

基於以上的緣由分析,咱們嘗試了比較多的方案。其中因爲Vuex數據流不合理帶來的過多rerender,因爲項目過於複雜,涉及到了互動題編輯器和模版編輯器,數據流的改動風險較大,並且收益不必定明顯。因而在3-4月的優化中,咱們沒有動現有的狀態管理,而是在當前基礎上,力求減小每次操做的計算量和渲染量,也就是在不合理的方案下去緩解用戶體驗問題。咱們的思路聚焦在:課件列表須要動態加載,頁面節點越少,掛載的組件越簡單,Vue的計算越快,瀏覽器渲染的速度也越快。 因而咱們作了如下嘗試:

  1. IntersectionObserver

借鑑圖片懶加載的方式,咱們經過瀏覽器的 IntersectionObserver 進行 dom 的監聽實現課件頁的懶加載

// 在須要懶加載的節點上添加ref屬性爲「containerNeedLazyLoad」

// 將控制是否進行加載的boolean變量命名爲「elementNeedLoad」

export default {

  data() {

    return {

      elementNeedLoad: false,

      elementNeedVisible: false

    };

  },

  mounted() {

    const target = this.$refs.containerNeedLazyLoad;

    const intersectionObserver = this.lazyLoadObserver(target);

    intersectionObserver.observe(target);

  },

  methods: {

    lazyLoadObserver(target) {

      return new IntersectionObserver((entries) => {

        entries.forEach((entry) => {

          if (entry.intersectionRatio > 0) {

            if (this.elementNeedLoad === false) {

              this.elementNeedLoad = true;

              this.$nextTick(() => {

                this.elementNeedVisible = true;

              });

              this.lazyLoadObserver().unobserve(target);

            }

          } else {

            this.elementNeedLoad = false;

            this.elementNeedVisible = false;

          }

        });

      }, {

        threshold: [0, 0.5, 1],

      });

    },

  }

};
複製代碼

image.png

簡言之就是咱們給200頁課件每一頁的dom容器元素都添加了一個監聽器,當該課件進入視窗時渲染內部的元素,在課件出視窗時註銷元素,經過v-if指令來實現。該方案實現後,實時渲染的課件數量只有視窗內的7-8個,其餘課件只渲染了容器組件,頁面節點也少了不少,當咱們點擊切換課件時變得流暢了許多,那是由於未徹底渲染的課件頁Vue組件都變得「簡單」了,vue的計算也會更快,點擊課件的時間能夠在300ms內響應。極大優化了體驗,因而咱們滿心歡喜上線了該優化。

  1. 動態加載

一天後,又有教研老師反饋頁面卡頓,列表頁常常滾動比以前更卡,因而咱們再次排查。上面的方案留下了一個比較大的問題是,列表頁在滾動的過程中,須要實時監聽dom,咱們不懷疑瀏覽器api的性能,可是v-if指令會在值變化時,執行虛擬dom的計算並更新組件,而這個過程是在滾動的過程當中實時進行的。

從上面的視頻中咱們能夠看到在滾動時很容易出現掉幀的狀況,因此圖片懶加載的方案沒法直接嫁接,咱們須要更好的懶加載方案。

在競品調研過程當中,咱們比較關注google doc和騰訊文檔的方案。

Google doc採用了滾動懶加載的方式加載文檔,可是因爲google的底層方案都是基於svg,方案沒法復刻,可是動態加載的方式能夠借鑑。

騰訊文檔則是採用了分頁加載,首屏渲染以後再加載其餘的內容,可是因爲咱們在 url 上攜帶了課件 id須要定位到具體的課件頁,並且數據方面沒有分頁,所以該方案暫時不考慮。此外咱們頁關注了某教研雲的課件加載方案:

與預想中的同樣,某教研雲也採用了動態加載的方式,可見這也算是長列表比較常見的優化手段。

因而咱們有了如下優化思路:

  1. 默認每張課件不渲染任何節點,只渲染容器節點,dom結構會簡單不少
  2. 監聽列表容器的滾動事件,計算當前視圖最中間的課件index,同時將上下各7張課件做爲可渲染課件,添加滾動監聽的防抖
  3. 添加可渲染課件index時經過 setTimeout 逐一添加實現流式加載
  4. 已經渲染的頁面在下一次滾動中再也不重複渲染
import { on, off, mapState, mapMutations } from 'utils';

import debounce from 'lodash/debounce';



/** * 總共加載當前課件的ppt上下若干頁課件,其餘的經過滾動延遲加載 */

const renderPagesBuffer = 7;

const renderPagesBoundary = 2 * renderPagesBuffer + 1;

const debounceTime = 400;

const progressiveTime = 150; // 漸進式渲染間隔時間



/** * 持久化一下 */

const bodyHeight = document.documentElement.clientHeight || document.body.clientHeight;



export default {

  data() {

    return {

      additionalPages: [],

      commonPages: [] // 先後兩次滾動所須要渲染的公共頁面

    };

  },

  mounted() {

    this.observeTarget = this.$refs.pprOverviewList;

    on(this.observeTarget, 'scroll', () => {

      this.handleClearTimer();

      this.handleListScroll();

    });

    

    if (!this.renderAllPages) {

      this.updateCurrentPagesInView(new Array(renderPagesBoundary).fill(1).map((_, i) => i));

    } else {

      /* 先手動觸發一次 */

      const timer = setTimeout(() => {

        this.handleClearTimer();

        this.handleListScroll();

        clearTimeout(timer);

      }, debounceTime * 2);

    }

  },

  beforeDestroy() {

    off(this.observeTarget, 'scroll', this.handleListScroll);

  },

  computed: {

    ...mapState('editor', ['currentPagesInView']),

    pagesLength() {

      return this.pptDetail?.pages?.length || 0;

    },

    renderAllPages() {

      return this.pagesLength > renderPagesBoundary;

    }

  },

  watch: {

    additionalPages(val) {

      this.observerIndex = 1;

      this.handleRenderNextPage();

    }

  },

  methods: {

    ...mapMutations('editor', ['updateCurrentPagesInView']),

    /** * 增長滾動事件的防抖設置,防止頻繁更新 */

    handleListScroll: debounce(function() {

      const { scrollTop, scrollHeight } = this.observeTarget;

      const percent = (scrollTop + bodyHeight / 2) / scrollHeight;

      // 找到當前滾動位置位於頁面中心的 ppt

      const currentMiddlePage = Math.floor(this.pagesLength * percent);

      const start = Math.max(currentMiddlePage - renderPagesBuffer, 0);

      const end = Math.min(currentMiddlePage + renderPagesBuffer, this.pagesLength + 1);



      // 已經渲染了的頁面集合(保證不重複渲染)

      const commonPages = [];

      // 滑動以後須要新渲染的頁面集合

      const additionalPages = [];



      for (let i = start; i < end; i++) {

        if (this.currentPagesInView.includes(i)) {

          commonPages.push(i);

        } else {

          additionalPages.push(i);

        }

      }

      this.commonPages = commonPages;

      this.additionalPages = additionalPages;

    }, debounceTime),



    handleRenderNextPage() {

      const nextPages = this.additionalPages.slice(0, this.observerIndex);

      this.updateCurrentPagesInView(

        [...nextPages, ...this.commonPages]

      );

      this.observerIndex++;

      if (this.observerIndex >= this.additionalPages.length) {

        this.handleClearTimer();

      } else {

        this.observerTimer = setTimeout(() => {

          this.animationTimer = requestAnimationFrame(this.handleRenderNextPage);

        }, progressiveTime);

      }

    },



    handleClearTimer() {

      this.observerTimer && clearTimeout(this.observerTimer);

      this.animationTimer && cancelAnimationFrame(this.animationTimer);

    }

  }

};
複製代碼

其中須要注意的時,滾動的監聽添加了400ms的防抖,咱們一直滾動會感覺到很是流暢,而在中止滾動開始渲染時,若是同時渲染計算得來的共15張課件,則在這些組件渲染完成以前頁面依然是卡死的狀態,所以咱們採用了setTimeout實現漸進式渲染,宏任務的好處就是讓咱們能夠在每一次事件循環中插入微任務,好比當前課件正在進行流式渲染,這時點擊了某張課件能夠先切換再繼續渲染。

this.observerTimer = setTimeout(() => {

  this.animationTimer = requestAnimationFrame(this.handleRenderNextPage);

}, progressiveTime);
複製代碼

另外當再次觸發滾動事件時,須要清除此前全部的定時器從新計算,而已經渲染了的頁面index咱們存在 commonPages 數據中,在下一次計算時不進行清除。最終優化的效果以下:

能夠看到從用戶體驗上,已經解決了滾動的卡頓問題,同時也不會由於組件過多阻塞用戶的點擊事件。

經過性能監控也能看到在滾動過程當中幾乎沒有任何的計算,這也是滾動起來十分流暢的緣由。

  1. 虛擬列表

咱們前面也說到,這是在現有數據流不合理的狀況下的無奈之舉,而要完全解決須要更完美的方案。因爲Vue框架沒有提供React的memo或者shouldComponentUpdate相似的鉤子函數,讓開發者決定組件是否應該從新渲染,所以在更完全的解決方案中,由組內另外一位同窗主導設計,咱們嘗試用react虛擬列表進行重構。

長列表的終極優化方案仍是要走向虛擬列表,不管是百度貼吧中期的技術重構,仍是今日頭條的feed流,都曾基於該方案作過探索。

虛擬列表是一種根據滾動容器元素的可視區域來渲染長列表數據中某一個部分數據的技術,具體在實現的時候,須要一個用於滾動監聽的虛擬容器,和一個用做元素渲染的真實容器。

虛擬列表經過計算滾動視窗,每次只渲染部分元素,既減小了首屏壓力,長時間加載也不會有更多的性能負擔。虛擬列表中的元素都是絕對定位的,不管列表有多長,實際渲染的元素始終保持在可控範圍內。

前端領域各個社區也有了比較成熟的解決方案,react-virtualized、react-window 以及 vue-virtualize-list 等等,關於虛擬列表原理的敘述能夠參考如下文章,這裏限於篇幅再也不贅述:

github.com/dwqs/blog/i…

import { SortableContainer } from 'react-sortable-hoc';

import { areEqual, VariableSizeList as List } from 'react-window';

import React, {

  useCallback,

  useEffect,

  useMemo,

  useRef,

  useState,

} from 'react';



export const ReactVirtualList: React.FC<any> = (props) => {

  const list = useRef(null);

  const initialScrolled = useRef(false);

  const [dragging, setDragging] = useState(-1);

  const { pages, currentPageId, selectedPagesId } = props;

  const pageGroups = buildPageGroups(pages);



  useEffect(() => {

    props.onReady({ scrollToTargetPage });

  }, []);



  useEffect(() => {

    list.current.resetAfterIndex(0);

  }, [pages]);



  useEffect(() => {

    // 進入頁面定位到選擇頁面

    if (!initialScrolled.current && pages.length > 0) {

      scrollToCurrentPage();

      initialScrolled.current = true;

    }

  }, [pages]);



  const scrollToCurrentPage = () => {

    scrollToTargetPage(currentPageId, pages);

  };

  

  // 渲染列表的每一項

  return <SomeThing />

 } 
複製代碼

同時因爲最初咱們縮略圖的元素渲染器採用了跟用戶操做區的同一個渲染組件,每一個元素上都有不少事件監聽,新版本咱們也封裝了基於 react 的純 ui 元素渲染器 ,去掉了無用的事件監聽,簡化了dom結構。

二者結合就成了新版本的縮略圖列表,目前已經上線完成,從各項指標以及用戶體驗來看,提高仍是很是大的,其中更明顯的拖拽排序,相較於此前的拖拽排序用戶體驗要好得多。

  1. 內存泄漏

一樣是上述課件,通過上述的優化,頁面的節點數量已經少了不少,並且卡頓問題也獲得了改善。可是當咱們不斷滾動左側預覽圖時,一段時間以後仍是會卡頓甚至卡死。此時要麼是 cpu佔用率太高致使頁面沒法響應,要麼出現了內存泄漏問題,通過排查發現,雖然頁面中的節點在懶加載過程當中會註銷,可是這些節點依然會被保存在內存當中,一直沒有釋放,甚至達到10w之多,內存佔用也一直線性增加,咱們須要針對這一些內存中的節點進行優化,處理內存泄漏問題。

咱們經過內存快照能夠看到,標記爲 Detached 也就是脫離文檔流的節點依然存儲在內存當中,其中 DIVElement 有兩萬多個,展開發現都是 element-render (咱們課件元素渲染器)中的元素,好比帶有 render-wrap 或者 selection-zone 的這些類(都是咱們項目中掛載在dom上的類名)。因此判斷這個組件存在內存泄露問題。

排查的過程當中,一度懷疑是vue 的 v-if 形成的,在 vue的官網有關於 v-if 內存泄漏的相關內容。

cn.vuejs.org/v2/cookbook…

我嘗試了經過手動掛載組件執行$mount,在課件滑到視圖以外時手動註銷,依然沒有做用,甚至嘗試實現自定義指令 v-clean-node,在懶加載過程當中動態註銷節點。但事實證實,節點是已經從dom結構中註銷了的,只是對應的dom片斷依然保存在內存當中,並且筆者也沒有找到能夠手動釋放內存的方法,至少在瀏覽器環境尚未辦法辦到。這裏走了不少彎路,而思考的方向或許也能成爲一些反面案例,不過性能優化這條路須要作的也是勇敢嘗試,勇於試錯,最終總能找到一個相對較優的解決辦法。

咱們換個思考的方向,既然不能手動釋放內存,就去迎合v8內存管理的原理,代碼怎樣寫才能保證內存被正常釋放。咱們想到的就是一直被引用的變量,其次就是未被註銷的事件監聽。

接下來的排查採用了打斷點和註釋代碼的笨辦法,逐漸縮小排查範圍,最終鎖定在了渲染富文本所使用的 render 組件。

這個組件用到了兄弟團隊提供的render 庫,而在 node_modules 是編譯後的 es5代碼,可讀性還不錯,因而我經過斷點在在源碼中進行調試,手動追蹤調用棧。

在最終渲染的方法中,排查找到了這樣一行代碼:

這裏每一個富文本渲染都會添加一個body resize的事件監聽,而筆者並無找到相關unobserve的邏輯。相似這種事件監聽的代碼若是沒有取消監聽,很容易形成內存泄漏,註釋這一行代碼以後重啓項目,系統的內存能夠正常回收了,最終肯定是由這個sdk致使了內存泄漏,後續兄弟團隊的同窗也協助解決了這一問題,從新發了一個正式包。

經過測試發現內存已經能夠正常釋放。

這裏的內存泄漏能夠用如下示意圖歸納:

image.png

未被釋放的事件監聽會致使對應的組件在卸載時並未被釋放,所以咱們的內存中會有這些 vue組件。

平常項目開發的過程當中,內存優化須要持續關注,咱們課件編輯器的內存佔用大概100M左右,須要在後續的開發過程當中持續優化

  1. 點擊預覽按鈕以後頁面操做卡頓

這是一個很是有趣的案例,課件的預覽是在當前頁面打開一個彈窗嵌入預覽頁面的iframe,每次點擊預覽以後回到編輯器總會出現或多或少的卡頓現象。

這是由於預覽頁和編輯器的域名相同,所以打開iframe時共享了同一進程。

iframe 做爲升級版的 frame,通常來講都會被認爲和上層的 parent 容器處在同一個進程中,他們會擁有父容器的一個子上下文 BrowserContext。在這種狀況下,iframe 當中的 js 運行時便會阻塞外部的 js 運行,特別是當若是 iframe 中的代碼質量不高而致使性能問題時,外層運行的容器會受到至關大的影響。這顯然是咱們不肯意看到的,由於 webview 中的內容僅僅會做爲 IDE 拓展機制的一部分,咱們不但願看到咱們的外部 UI 和程序被 iframe 阻塞從而致使性能表現不佳。

iframe 線程

幸運的是,Chrom 在 67 版本以後默認開啓了 Site Isolation。基於它的描述,若是 iframe 中的域名和當前父域名不一樣(也就是你們熟悉的跨域狀況),那麼這個 iframe 中的渲染內容就會被放在兩個不一樣的渲染進程中。而咱們只須要將 IDE 主應用的頁面掛在域名A下,而同時將 iframe 的的頁面掛在域名B下,那麼這個 iframe 的進程就和主進程分開了。在這種模型下,iframe 和主進程僅僅能經過 postMessage 和 message 事件進行數據通信。可是在上面的模型中,仍然有一點須要注意。基於 Site Isolation 的特性,同一個頁面中若是有多個,擁有同一個域名的多個 iframe 之間是共享進程的,所以他們仍然會互相影響性能。若是某個業務場景須要一個更爲獨立的 iframe 進程,它必須和其餘 iframe 擁有不一樣的域名。

咱們在項目中分別嵌入了百度首頁和咱們課件渲染頁面,發現一級域名相同時iframe和當前頁面老是會共享進程id,不管嵌入頁面的性能如何,對當前頁面都會有或多或少的影響。所以咱們有了如下解決方案:

  1. 預覽頁面部署到新的域名上,二者不共享進程
  2. 經過a標籤打開新的頁面進行預覽,須要注意的是a標籤須要加上 rel="noopener"屬性,切斷進程聯繫
<a

  v-if="showOpenEntry"

  class="intro-step3 preview-wrap"

  rel="noopener"

  target="__blank"

  :disabled="!showEditArea"

  :style="{ color: !showEditArea ? 'lightgray' : '#515a6e' }"

  :href="pageShareUrl"

>

  <lego-icon type="preview" size="16" />

</a>
複製代碼

目前的優化中採用了第二種跳轉的方式做爲臨時方案。

  1. 添加動畫時間過長

有這樣一個場景是老師須要給多個元素同時添加動畫,可是頁面須要幾秒鐘響應,對於用戶來講就是出現了卡頓。

咱們依然經過performance排查,肯定了動畫表單的渲染阻塞了ui的更新,一樣涉及到長列表,但此處沒有課件列表複雜,並且課件頁都渲染了一個固定高度的容器,此處每一個動畫表單高度都不同,所以咱們採用另外一種懶加載的渲染方式:

export default {

  data() {

    return {

      // 當前渲染動畫數量

      nextRenderQuantity: 0

    };

  },

  

  computed: {

    animationLength() {

      return this.animationConfigsUnderActiveTab.length;

    },

    renderAnimationList() {

      // 從原數組中切割

      return this.animationConfigsUnderActiveTab.slice(0, this.nextRenderQuantity);

    }

  },



  watch: {

    animationLength: {

      handler() {

        this.handleRenderProgressive();

      },

      immediate: true,

    }

  },



  beforeDestroy() {

    this.timer && cancelAnimationFrame(this.timer);

  },

  

  methods: {

    /** * 動畫表單的漸進式渲染,每一幀多渲染一個 */

    handleRenderProgressive() {

      this.timer && cancelAnimationFrame(this.timer);

      if (this.nextRenderQuantity < this.animationConfigsUnderActiveTab.length) {

        this.nextRenderQuantity += 1;

        this.timer = requestAnimationFrame(this.handleRenderProgressive);

      }

    },

  }

};
複製代碼

經過 requestAnimationFrame 每一幀添加一個動畫,也就是40個元素同時添加動畫,須要40x16 = 640ms渲染完表單,而在這個時間以前,頁面已經及時做出了響應,老師在使用的時候就不會以爲卡頓了。

優化先後的響應時間從2.35s => 370ms,優化效果比較顯著。

總結:不要讓你的js邏輯阻塞了ui的渲染。

  1. 其餘的優化

其餘的一些常見的項目優化就不在此贅述了,不管什麼樣的業務場景,tob仍是toc系統,你們可能都曾在如下優化方向上摸爬滾打過。

  1. 路由懶加載
  2. 靜態資源緩存
  3. 打包體積優化
  4. 較大第三方庫藉助cdn
  5. 編碼優化:長數組遍歷善用 for 循環等
  6. 可能的預編譯優化

深刻框架,尋找性能優化方向

Vue 的懶人哲學 vs React 暴力美學

在性能優化的路上越走越偏,我也深深感覺到前端工具帶來的便利和過度依賴前端框架所帶來的所謂的 side effect。vue 和 react 做爲現在最火的兩個框架,各自有着其獨特的魅力,而性能優化的同時咱們始終繞不開框架的底層原理。

Vue 的懶人哲學:

曾經的一次分享中咱們提到了vue所謂的懶人哲學 ,也就是說 vue 框架內部爲你作了太多優化工做。

咱們知道Vue會對組件中須要追蹤的狀態,將其轉化爲getter和setter進行數據代理,構建視圖和數據層的依賴,這就是ViewModel 這一層。而正是因爲vue精確到原子級別的數據偵聽使得其對數據十分敏感,任何數據的改變,vue都能知道這個數據所綁定的視圖,在下一次dom diff時,他能精確知道哪些dom該渲染,哪些保持不動。而vue的這個原理也是他升級Vue3時進行更高效的預編譯優化的前提條件,感興趣的同窗能夠跟我探討下曾經的分享,這其中也聊到了 Vue。

zhuanlan.zhihu.com/p/158880026

可是最大問題在於,vue 更新視圖剛好很少很多的前提是,你的數據流十分乾淨,沒有多餘的數據更新,不然「敏感」的vue會覺得更多的組件須要從新渲染,這也是目前咱們課件編輯器的問題所在,項目體量愈來愈大,幾乎沒有幾個開發者能夠保證本身所維護的狀態管理乾淨透明,而一旦有不合理的數據更新,組件的從新渲染是沒法從中攔截的,所以用 Vue 可讓你「懶」一點,也須要你寫代碼時「當心」一點。而對比react,二者底層設計的不一樣致使在遇到此類問題時,咱們可能須要不同的思考方向。

如今在知乎上還能翻到一些尤大關於 react 性能問題的理解。

尤大說的比較通俗易懂,並且也確實直指react框架的要害,其中所提到的 react 把大量優化責任丟給開發者,相信你們都有所感覺。

React 的暴力美學:

與Vue不一樣的是,React對數據自然不敏感,框架不關心你更新了多少數據,乃至更新了多少髒數據,數據與dom結構也沒有vue那種依賴關係,你只須要經過 setState 告訴我我應該渲染哪些組件便可。與 Vue 相比,react的處理方式既優雅又暴力,並且從開發者的角度來審視,react的這種設計真的減小了太多的心理負擔,而做爲初接入react的開發者來講,你不會由於多更新了數據致使過多的rerender 而抓耳撓腮,你要作的就是藉助框架自己去消除這些反作用,衆所周知react正好提供了這些能力:

  • Reat.memo
  • PureComponent
  • shouldComponentUpdate

你須要的,都能借助框架或者第三方工具作到。

談到這裏,不妨具體說說框架如何避免沒必要要的渲染問題:

以 react context 爲例,若是咱們在項目中全部的狀態管理都放在一個 context 中,那麼在使用時總會引發沒必要要的渲染。而在開發過程當中如何避免,不一樣開發者都有不一樣的心得。

const AppContext = React.createContext(null);



const App = () => {

  const [count, setCount] = useState(0); // 常常變的

  const [name, setName] = useState('Mike'); // 不常常變的

  return (

    <AppContext.Provider value={{ count, setCount, name, setName }}> <ComponentA /> <ComponentB /> </AppContext.Provider>

  )

}



const ComponentA = () => {

  console.log('這是組件A');

  const context = useContext(Context);

  return (

    <div>{context.name}</div>

  )

}



const ComponentB = () => {

  console.log('這是組件B')

  const context = useContext(Context);

  return (

    <div> {context.count} <button onClick={() => { context.setCount((c) => c + 1) }} > SetCount </button> </div>

  )

}
複製代碼

在這個 demo中,咱們在頂層注入了 Context 作狀態管理,同時有一個常常改變的狀態count 和一個不常常改變的狀態 name,組件A和組件B分別引用了 name 和 count 這兩個狀態。

咱們在點擊 SetCount 時,調用了全局上下文 中的方法,同時觀測到A B兩個組件都會從新渲染,而實際上咱們的組件 A只用到了 name 這個狀態,是不該該從新渲染的。這裏的數據流其實很是「乾淨」,沒有多餘的引用,若是是 Vue,它會追蹤依賴而避免組件 A 的渲染,react 卻沒有作到。而做爲 react 開發者,若是聽任這種沒必要要的 rerender無論,那正如尤大所說, react 應用的性能確實會遇到瓶頸,好在 react 給了開發者足夠的發揮空間,大多開發者遇到此類場景,反手就是一個 context 拆分優雅解決:

const AppContext = React.createContext(null);

const AppContext2 = React.createContext(null);



const App = () => {

  const [count, setCount] = useState(0);

  const [name, setName] = useState('Mike');

  return (

    <AppContext.Provider value={{ name, setName }}> <AppContext2.Provider value={{ count, setCount }}> <ComponentA /> <ComponentB /> </AppContext2.Provider> </AppContext.Provider>

  )

}



const ComponentA = () => {

  console.log('這是組件A');

  const context = useContext(Context);

  return (

    <div>{context.name}</div>

  )

}



const ComponentB = () => {

  console.log('這是組件B')

  const context = useContext(Context);

  return (

    <div> {context.count} <button onClick={() => { context.setCount((c) => c + 1) }} > SetCount </button> </div>

  )

}
複製代碼

這裏咱們將兩個狀態拆分進不一樣的 context 中,此時再調用 setCount 方法,就不會影響到組件 A 從新渲染了。這也是咱們實際項目開發中最多見的解決方案。可是項目體量愈來愈大時,這種模塊的拆分會變得很繁瑣,相似 Vuex 模塊的拆分同樣,咱們開發一個新功能,也老是不肯意在 vuex 中新開闢一個模塊,寫更多的文件以及 getters mutations。因此在這個例子中咱們也能夠經過 useMemo 來解決:

const ComponentA = () => {

  console.log('這是組件A');

  const context = useContext(Context);



  const memoResult = useMemo(

    () => {

      <div>{context.name}</div>

    },

    [context.name]

  )



  return memoResult;

}
複製代碼

咱們在組件 A 中,組件內容用 useMemo 包裹,將其製造爲一個緩存對象,這裏的 useMemo 不去緩存 state,由於咱們調用了頂層方法 setCount 引發 state immutable 更新 -> 進而 name 更新(引用地址變化),在頂層組件中緩存 state 其實並無什麼用,因此在這個案例中 useMemo 只能用來緩存組件。

固然,咱們不能每一個組件都經過 useMemo 來處理,不少時候只是平添開銷。所以 react 團隊所提出的 context selectors 纔是解決相似案例的最佳選擇:

github.com/reactjs/rfc…

經過 selector 機制,開發者在使用 Contexts 時,能夠選擇須要相應的 Context data,從而規避掉「不關心」的 Context data 變化所觸發的渲染。這跟咱們手動拆分 context 所實現的功能一模一樣,總的來講優化思路仍是比較一致的。

react 更多案例能夠參考:

codesandbox.io/s/react-cod…

聊到這裏咱們發現,當使用框架做爲開發工具來解決問題時,若是產生了反作用,react開發者有不少方式能夠抵消這個反作用,而相對來講 vue 以及vue生態圈所能提供的解決方案就比較少,正如前面咱們遇到的那些bad case同樣,咱們可能須要從一些比較偏的角度去思考才能解決這類問題。

題外話(我的觀點):

我的認爲 Vue 作大型項目有着自然的弊端,因爲遞歸實現了精確數據偵聽,使得其產生了過多的訂閱對象,而正如前面所說,一旦數據流不合理,多餘的更新不可逆,而過多的偵聽對象對系統內存也是一個考驗。令一點就是筆者的自我感覺,Vue 項目開發在組件化不如 React 來得清澈透明,單文件組件大了以後,可讀性也比較差,而react 有社區加持,有 Redux 和 Saga 進行狀態管理,上手曲線雖然略高,可是代碼規範度極高,狀態管理效果極好,適合團隊開發。反觀 vue, 作小型項目卻有着自然優點(爲何?),所以每一個項目在前期都要着重分析業務場景,作好項目規劃和技術選型。我的觀點,但願各位同窗指出不足,理性討論。

以上是筆者在咱們課件編輯器的項目中一些優化實踐,不一樣場景有不一樣的解決方案,但願你們也能夠留言給到一些建議和幫助,讓咱們課件團隊能夠打磨出更好的產品。

關於性能優化的建議

  1. 面向用戶,瞭解用戶真正的痛點

縮小產研團隊和用戶之間對產品理解的gap。

  1. 發現問題,問題就解決了一半

發現問題,也須要發現問題的根源,性能問題的背後,每每是編碼的不合理以及工具的不合理應用。

  1. 概括總結,舉一反三。

類似的問題千篇一概,有趣的方案各有各的特點,每次性能優化以後,概括總結總能帶來更多的收穫。

❤️ 謝謝支持

以上即是本次分享的所有內容,但願對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~。

歡迎關注公衆號 ELab團隊 收貨大廠一手好文章~

咱們來自字節跳動,是旗下大力教育前端部門,負責字節跳動教育全線產品前端開發工做。

咱們圍繞產品品質提高、開發效率、創意與前沿技術等方向沉澱與傳播專業知識及案例,爲業界貢獻經驗價值。包括但不限於性能監控、組件庫、多端技術、Serverless、可視化搭建、音視頻、人工智能、產品設計與營銷等內容。

歡迎感興趣的同窗點擊內推連接內推到做者部門拍磚哦 🤪

相關文章
相關標籤/搜索