小斑瘦身記:4M 到 300K 的華麗變身

console.info

Hello ~ 我是小斑,一個富文本編輯器。今天,咱來聊聊體重,對,沒錯!就是那使人頭疼的體重!任何事物都煩這個體重,固然也包括我,當個人創造者阿飛發佈小斑的第一個版本時,小斑足足有 4M 之重!看着控制檯茫茫多的流量消耗,他愣住了:你。。。特麼的這麼怎麼胖,足足 4M(包括 CSS)!但小斑我也很無奈呀!代碼是你寫的,怪我咯!javascript

軟件發展總歸避免不了兩個過程:野蠻生長 & 精心優化,小斑長算是長出來了,但這一身肥膘得好好減減!css

image

噥!上圖,就是我剛誕生時,所包含的 JS 模塊,足足 3M 之多!但小斑的核心代碼僅僅存在於右下角藍色的方塊內,其餘都屬於加強體驗的代碼,包括 UI 框架,代碼高亮,表情的便捷輸入等等。html

接下來,小斑瘦身記,正式開始。java


數據壓縮:從 4M 到 1.5M

提到數據壓縮,就不得不提一個鼎鼎大名的數據壓縮算法:Gzipreact

Gzip 全名: GNU zip 開源的數據壓縮算法,普遍應用於網絡傳輸。你們可能會有疑惑,數據壓縮算法這麼多,憑啥 Gzip 如此出名?由於壓縮比高?webpack

no!no!no!僅僅由於它是 GNU 開頭,互聯網誕生於一個專利橫飛的年代,各類好用的壓縮算法受限於專利,並不能使用在開放的互聯網上,所以你們須要一個高效且開源的壓縮算法,Gzip 所以誕生!git

使用 Gzip 很簡單,過程大概也就 3 步:github

  1. 服務端使用 Gzip 將須要發送的數據進行壓縮;
  2. 客戶端將接收的數據並進行 Gzip 解壓;
  3. 客戶端使用解壓後的數據;

所以,Gzip 是服務端對輸出內容的優化,且須要客戶端支持(客戶端須要有解壓的能力)。web

等等!須要客戶端支持?會不會很麻煩?算法

固然不!別忘了 Gzip 的全名是 GNU zip ,它但是開源的!只要客戶端內置 Gzip 模塊,就能夠徹底使用 Gzip 壓縮後的數據!所以能夠簡單的認爲:只要服務端進行了 Gzip 壓縮,文件的體積能瞬間減小一半!

至於如何開啓 Gzip ,各個服務平臺都有與之相對應的方法。小斑由 Nginx 提供服務,開啓 Gzip 模塊後,小斑體積驟減至 1.5M ,看起來像個瘦子了!但也僅僅是看起來!我依然是個胖子,只不過神奇的壓縮算法把我給變瘦了,濾鏡下的胖子,再怎麼好看依舊是個胖子!

ps:並非全部的瀏覽器都內置了 Gzip 模塊,IE 系列就沒有(你們趕忙拋棄 IE 吧),所以真實環境下客戶端須要一些特殊的請求頭,來控制服務器返回的具體內容。


移除冗餘代碼:銳減 1.4M

冗餘代碼:就是那些打包到項目中,卻沒被使用的代碼。看到這個標題,有些小夥伴可能會心生不屑,哼!這人連 Webpack 支持 TreeShaking 了都不知道,也不知道是用那個版本的 Webpack,還在說這些老掉牙的問題,真沒意思!

稍等!小斑誕生環境中的 Webpack 早已 4.42.0,徹底支持 TreeShaking,在這裏,小斑只想問一個簡單的問題:TreeShaking 能解決樣式冗餘嗎?

不能,直到如今爲止,依然沒有一種較好的辦法,在代碼打包時,解決樣式冗餘的問題。但在代碼打包前,也就是編寫代碼的時候,卻能夠!

一行代碼,700K 的體積!

相信使用過 Antd 組件庫的小夥伴,使用 Antd 時第一行代碼都長這樣:

import "antd/dist/antd.css";

但我悄悄告訴你,這個文件,整整 700K !請問:有何感想?

小斑爲此深刻查看了 Antd 代碼,瞭解到其實每一個組件下都有單獨的樣式文件,單獨引入樣式,就能能夠移除多餘 CSS 代碼。示例以下:

// antd 的公共樣式,必須引入
import "antd/lib/style/core/index.less";

// 組件樣式,單獨引入便可
import "antd/lib/button/style/index.less";
import "antd/lib/xxx/style/index.less";

考慮到,編輯器內,用到了一些高級特性,須要較高版本瀏覽器的支持,故只考慮了最近幾個版本的 Chrome(包括使用 Chromium 核心的其餘瀏覽器)FireFoxSafari ,一些已經成爲標準的 CSS3 樣式前綴就不必兼容了,所以小斑的兼容性以下:

"browserslist": [
     "last 1 chrome version",
     "last 1 firefox version",
     "last 1 safari version"
 ]

不要問小斑爲何不兼容 IE,連它親爸都不要它了,兼容它幹嗎!

一番下來,小斑的樣式獲得了最大程度的精簡(由於 Antd 組件內的樣式包含了組件的全部樣式,會有部分冗餘),樣式體積驟減,由本來 900K 減至不到 200K!通過 Gzip 發生到客戶端的體積,不到 100K

想高亮嗎?收下這 1M 代碼!

在小斑誕生之初,便已肯定,編輯器必須支持代碼高亮,那天然就須要引入 highlight.js,但這句代碼比 Antd 的樣式代碼更加誇張,好嘛!整整 1MJS!看到上圖中那個大大的 highlight.js 代碼塊了嗎?臃腫不堪,經仔細研究後發現,核心的高亮代碼其實不多,絕大部分被 languages 佔用!

怎麼辦?去官網瞧瞧唄,得嘞,不看不知道,一看嚇一跳,經過如下形式引入的 highlight.js 包含了整整 189 種的語言解析器!

import hljs from 'highlight.js';

什麼?爲啥 TreeShaking 沒起做用?這和 TreeShaking 的關係可真心不大,高亮是在打包後纔用到的模塊!那該如何精簡?其實官方已經給了答案:按需加載,僅加載須要引入的語言解析器,甚至官方已經爲咱們推薦好了經常使用的 38 種語言,一一注入便可!

import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';

hljs.registerLanguage('javascript', javascript);

雖然總體的代碼量因爲須要註冊 38 種語言增長了一點點,但換來的倒是 1M300K 打包體積的變化,值嗎?很是的值!

image

Δ 移除很是用語言後的代碼高亮後的總體打包體積。highlight.js 僅佔了其中的一小塊。


不變的內容,靜態化!

有用過小斑編輯器應該知道,小斑支持選擇代碼的高亮風格以及文章的主題。一開始,小斑包含這部份內容,但後來發現,每次發版,這部份內容永遠不變!高亮風格和文章主題不過是一段樣式信息而已,爲何不靜態化?把這部份內容獨立出去!這樣不只小斑可使用,你們也均可以使用了!

最後,阿飛又折騰出一個新項目:zebra-editor-theme 專一於文章樣式的管理,但願你們能多多關注!

固然,開源意味着共享,站點內全部的代碼高亮風格,以及文章主題,你們均可以獲取,使用,一下是文章主題以及代碼風格的樣式文件獲取方式,固然來目前的文章主題還不多,歡迎你們多多 PR ~


異步:並非全部的代碼,都須要及時加載!

冗餘,靜態代碼已優化完畢,看着這依然有 2.5M 的小斑,阿飛嘆了口氣:哎,你個胖子啊,看來不能一口氣把全部的代碼都給你。異步加載模塊得用上了!

模塊異步加載

異步加載是什麼?

一種延遲加載的技術,可讓資源在須要加載的時候,才進行加載。

如何實現?

很簡單!一句話的事:

const loadMdAst = () =>
  import(
    /* webpackChunkName: "mdast", webpackPrefetch: true */ "@textlint/markdown-to-ast"
  );

如上,就動態的引入了 Markdown 語法解析器。

如何使用?

import 函數返回 Promise,該 Promise 會在資源加載結束後進入 resolve 狀態,就能得到模塊的默認導出了!

const mdAstParse = await loadMdAst();

如上就能在 async 函數中獲取 Markdwon 的解析器。但該種方式有個弊端,因爲異步函數是有傳染性的!會致使一層層的函數都變成異步函數。

React 異步組件

import {lazy, Suspense} from "react";

const Panel = lazy(() =>
  import(/* webpackChunkName: "color", webpackPrefetch: true */ "./color-panel"),
);

const ColorBtn = () => {
  return (
    <Suspense fallback={<Loading size={80} />}>
      <Panel />
    </Suspense>
  )
}

一個簡單例子:顏色選擇框經過異步組件的形式,加載到項目中,相關內容可翻閱文檔:React-Suspense

異步,真的值嗎?

異步的模塊或是組件能夠延緩加載時間,但卻會致使兩個問題:

  • 代碼被異步污染,任何使用異步加載模塊的地方,都會被污染成異步函數;
  • 並無真正的減小請求的體積,但卻增長了請求數;

對於第一點,若是說在 async 函數出來以前,大部分小夥伴心裏估計是抗拒的,本來流程明確的代碼,被套用在一個 Promise 鏈裏,想一想都不舒服,不過如今 async/await 函數規範以及制定,異步的獲取結果也就一個 await 的事,一點也不麻煩!因此目前,第一點能夠忽略不計!

關於第二點,這麼說,若是項目中的模塊首屏就會被用到,雖然使用了異步的形式,但卻依然要等到模塊加載完畢才能展現首屏,這類異步的模塊其實沒有必要。

但若是說項目中有個功能,使用者始終不會用到,不加載應該是最好的選擇。還有,若是該功能,不會出如今首屏,但卻須要長達 1s 的加載時間,那麼它以一種後臺加載的形式,默默的在用戶使用的時候加載好,對用戶來講,也是很是棒的體驗了!

所以,對於異步模塊,只要知足這兩點中的一點,就可使用了:

  1. 首屏用不到的模塊;
  2. 須要在必定狀況下才顯示或使用的模塊;

異步模塊,雖然不能減小小斑的代碼總量,但卻能讓小斑早一點與大家相見。你說值嗎?固然很是的值呀!


緩存:並非全部的代碼,都須要再次加載!

熟悉 Webpack 的小夥伴都應該知道,Webpack 擁有代碼分類打包的能力,具體到 Webpack4 其實就是 optimization.splitChunks 這一塊你們討論的不少了,複製粘貼官網的文檔也沒有意義,就很少說了。在這小斑推薦你們結合官網和網上總結一塊兒看,由於網絡上文章的並不必定是最新的,記錄下爲何要代碼分塊(知其因此然比知其然更加劇要!):

  • 瀏覽器有本身的緩存策略,分爲強緩存和協商緩存,緩存生效的前提在於內容不變;
  • 因爲項目代碼並不是一層不變,所以爲了不緩存致使的問題,須要生成版本號;
  • 因爲版本號的不一樣,瀏覽器的緩存策略失效了;
  • 但項目的公共代碼(好比說 UI 庫)實際上是能夠緩存的;
  • 若是這些公共代碼分包成一個單獨的 bundle 不添加版本號,就能被瀏覽器緩存了!
  • 但公共代碼也會變呀!好比說修復漏洞,增長新功能等!
  • 那仍是得生成版本號!若是根據打包出來的內容生成版本號,問題迎刃而解!

分包,說到底,是讓瀏覽器緩存生效的一種策略,若是說全部的代碼都在一個 bundle 中,爲了不緩存致使的影響,不得已須要添加版本號,但分包以後就不一樣了,即便版本須要更新,一些包的內容其實沒有發生變化,打包出來的文件名也就不會變化,這極大的利用了緩存!

image

Δ 經異步加載,內容分包後的最終成果

首屏須要加載的內容塊以下:

  • willChange: 一些可能會發生變化的項目依賴,如 antd,由於可能會使用新的組件;
  • library: 固定不會的項目依賴,如 react
  • main: 小斑的核心代碼;

未經 Gzip 前,一共:554K + 344K + 338K ≈ 1MGzip 後僅僅 340K 小斑瘦身大獲成功!可是,一切並無結束!


終極利器 Service Work:0K!

Service Work 能夠簡單理解爲一個介於客戶端和服務器之間的一個代理,客戶端的請求會經過 Service Work 對外發送,並獲取內容,所以 Service Work 具備了控制返回內容的權利,它能夠緩存返回的內容,並在第二次請求的時候直接返回數據,這點和緩存很像,但區別就在於,它是可控的,甚至能夠提早獲取資源,並緩存!

想像一個具體場景,你的網頁發版了,用戶焦急的等待着瀏覽器下載新版網頁的資源,但一旁使用 Service Work 的網站依舊瞬間打開了老版頁面,開始瀏覽了起來,看起來好像你隔壁的網頁沒更新。但次日,你卻發現隔壁的網頁,不只更新到了最新版,並且依舊瞬間打開!做爲一個積極向上的開發者,我想,你應該理解到了這在用戶體驗上的差距!

Service Work 能夠檢查更新,輸出原先內容的同時,下載最新內容,並在合適的時候更新緩存內容,對於用戶來講,除了第一次訪問,以後的每一次都是瞬間打開,徹底不須要下載新內容!

這時候有些小夥伴就會問了:這麼牛逼的技術,必定很難咯!不!它不只不難,還很簡單,簡單到若是你使用腳手架,都不須要考慮這個問題!

因此,你要作的僅僅是打開腳手架裏的 Service Work 選項 ~

最後

囉囉嗦嗦寫了將近 6000 字,固然,還有一些好比 JS 的代碼壓縮插件 TerserPluginCSS 的壓縮插件 MiniCssExtractPlugin 之類的不提也罷,都是形式化的東西,這些腳手架已經幫開發者弄好了,不須要考慮,固然瞭解仍是須要了解的,那就先把 Webpack 文檔看一遍吧 ~

雖然文章僅用了幾天寫成,但具體到小斑的優化過程倒是是一個較長的過程,總結不易,給個 Star 唄。

本篇文章由斑馬編輯器編輯並生成,我是小斑,我爲本身帶鹽 ~

相關文章
相關標籤/搜索