負責任地編寫JavaScript代碼(二)

  • 原文地址:alistapart.com/article/res…
  • 原文做者:Jeremy Wagner
  • 譯者:馬雪琴
  • 聲明:本翻譯僅作學習交流使用,轉載請註明來源

你和開發團隊的成員熱情遊說老闆贊成對公司的老網站進行全面的重構,大家的請求被管理層甚至是最高管理層都聽到了,他們贊成了。高興之餘,你和團隊開始與設計、IA 等團隊一塊兒工做。沒過多久,大家就寫出了新代碼。javascript

重構工做一開始很是簡單,就是處處安裝 npm,這其實就是在快速安裝生產依賴項,就像一個大學生在作桶支架,而不關心次日早上的狀況同樣。css

而後,你就啓動了。html

與大多數豪飲的後果不一樣,痛苦並非次日早上就開始的。可是……幾個月後,產品全部者和中層管理人員開始感到噁心和頭痛,他們想知道爲何產品推出以來,轉化率和收入都降低了。而後事情會惡化到極點,CTO 週末從度假小屋回來,質問爲何網站加載速度如此之慢——若是它真的加載過。java

重構時每一個人都很開心,重構後沒有人快樂了。歡迎來到你的第一個 「JavaScript 宿醉」。node

這並非你的錯

當你與嚴重的「宿醉」做鬥爭時,「我告訴過你」這句話將是你應得的,它表明了激怒和指責——假設你還能夠在如此糟糕的狀態下戰鬥。react

說到 」JavaScript 宿醉」,不少人要爲此承擔責任,但相互指責只是在浪費時間。當今的網絡環境要求咱們擁有比競爭對手更快的迭代速度,這種壓力驅使咱們可能會利用任何可用的手段來儘量地提升生產力,所以,咱們更有可能(但也不必定)構建出開銷更大的應用程序,並可能會使用影響性能和可訪問性的開發模式。webpack

Web 開發並不容易,它是一個漫長的過程,咱們不多在第一次嘗試時就取得成功。然而,web 工做最好的地方也在於,咱們沒必要一開始就把它弄得很完美,咱們能夠在過後進行改進,這正是本系列的第二部分的目的所在。要達到完美還有很長的路要走,如今,讓咱們在短時間內經過改進站點的腳原本減弱 「JavaScript 宿醉」。git

把慣犯抓起來

基本的優化列表可能看起來很機械,可是值得一試。大型開發團隊,特別是那些跨多個庫工做,或不使用優化樣板文件的團隊,很容易忽略這些。es6

搖樹優化

首先,確保您的工具鏈配置了 tree shaking。若是你對 tree shaking 還不熟悉,我去年寫了一篇 tree shaking 指南,你能夠參考一下。簡而言之,tree shaking 是指將代碼庫中未使用的代碼再也不打包到生產包中的過程。github

現代的一些打包工具,如 webpack, Rollup 以及 Parcel 都有現成的 tree shaking 功能。GruntGulp 只是任務運行器,並非打包工具,因此它們沒有 tree shaking。任務運行器不會像打包工具那樣構建一個依賴關係圖,相反,它們根據提供的配置文件,用許多的插件來執行離散的任務。任務運行器可使用插件進行擴展,因此你能夠經過綁定打包工具來處理 JavaScript。若是這種方式對你來講存在問題,那麼你可能就須要手動審計並刪除未使用的代碼。

要想讓 tree shaking 生效,須要知足下面幾個條件:

  • 項目裏安裝的包以及編寫的邏輯必須是 ES6 模塊,對 CommonJS 模塊是不能進行 tree shaking 的。
  • 打包工具在構建階段不容許將 ES6 模塊轉換成別的模塊格式。在使用 bable 做爲工具鏈時,@babel/preset-env 配置必須指定 module:false,以防止 ES6 代碼被轉換爲 CommonJS。

Tree shaking 在構建過程當中不太可能沒有做用,若是真的沒有,那就讓它發揮做用。固然,它的有效性也因狀況而異,它還取決於你導入的模塊是否會引入反作用,這些反作用可能會影響打包工具刪除未使用的導出模塊。

代碼拆分

你頗有可能正在使用某種形式的代碼拆分,可是使用的方式值得從新評估。不管你如何拆分代碼,有兩個問題必定須要注意:

這些都很重要,由於減小冗餘代碼對性能相當重要。延遲加載能夠經過減小頁面初始 JavaScript 大小來提升性能。使用諸如 Bundle Buddy 之類的分析工具能夠幫助你發現是否存在代碼冗餘問題。

Bundle Buddy 能夠檢查 webpack 的編譯統計數據,並明確你的 Bundle 之間共享了多少代碼

在考慮延遲加載時,很難知道從哪裏開始。當我在現有項目中尋找時,我會在整個代碼庫中搜索用戶交互點,例如單擊和鍵盤事件,以及相似的候選項。任何須要用戶交互才能運行的代碼均可能是動態加載的好的選擇。

固然,按需加載腳本可能會顯著延遲交互性,由於必須先下載交互所需的腳本。若是不關心數據使用狀況,能夠考慮使用 rel=prefetch 資源提示以較低的優先級加載這些腳本,這些腳本就不會與關鍵資源爭用帶寬。rel=prefetch 的支持度很好,而且即便瀏覽器不支持它,也不會有任何問題,由於瀏覽器會忽略它們不理解的標記。

外化第三方託管代碼

理想狀況下,你應該儘量多地自託管站點的依賴項。若是因爲某種緣由必須從第三方加載依賴項,請在打包工具的配置中將它們標記爲外部包,不然可能會致使你網站的訪問者將從本地以及從第三方託管下載相同的代碼。

讓咱們來看一個可能會出現的假設狀況:假設你的站點從公共 CDN 加載 Lodash,你還在本地開發的項目中安裝了 Lodash,可是,若是你沒有將 Lodash 標記爲外部的,那麼你的產品代碼最終將加載它的第三方副本,而不是綁定的本地託管副本。

若是你瞭解你的代碼塊,這彷佛只是一個常識,但我看過這一常識被開發者忽視,這的確是值得你花時間檢查確認的一件事情。

若是你不相信能夠自行託管第三方依賴項,那麼能夠考慮爲它們添加 dns-prefetchpreconnect 甚至 preload 提示。這樣能夠減小站點的交互時間,若是 JavaScript 對呈現內容相當重要,則能夠減小站點的速度指數

更小的選擇,更少的開銷

Userland JavaScript 就像一個大得使人髮指的糖果店,咱們做爲開發人員,對大量的開源產品感到十分敬畏,框架和庫容許咱們快速擴展應用程序,實現原本須要花費大量時間和精力的各類各樣的功能。

雖然我我的傾向於在項目中儘可能減小客戶端框架和庫的使用,但它們的價值是引人注目的。然而,咱們確實有責任在咱們安裝的東西上採起強硬的態度,當咱們構建並交付了一些依賴於大量已安裝代碼來運行的東西時,就表明咱們接受了只有這些代碼維護者才能實際去解決一些問題,對吧?

多是也可能不是,這取決於所使用的依賴項。例如,React 很是流行,但 Preact 是一個很是小的替代品,它基本上擁有和 React 相同的 API,並與許多 React 插件兼容。Luxondate-fnsmoment.js 更簡潔,但也不是很小。

Lodash 這樣的庫提供了許多有用的方法,然而,其中一些很容易被原生 ES6 取代。例如,Lodash 的 compact 方法能夠替換爲 filter 數組方法。咱們其實並不不須要引入大型工具庫,咱們能夠輕鬆地替換更多

不管你喜歡的什麼樣的工具,思想都是同樣的:作一些研究,看看是否有更小的選擇,或者原生的語言特性是否就能夠達到這個目的。你可能會驚訝地發現,要真正地減小應用程序的開銷其實很簡單。

差別化腳本服務

你頗有可能在工具鏈中使用 Babel 將 ES6 源代碼轉換爲能夠在傳統瀏覽器上運行的代碼,這是否意味在傳統瀏覽器徹底消失以前,咱們必定要給根本不須要它們的瀏覽器提供巨大的代碼包?固然不是!差別服務經過將 ES6 源碼生成兩個不一樣版本的代碼包,能夠幫助咱們解決這個問題:

  • 代碼包1,它包含在較傳統瀏覽器上運行站點所需的全部轉換和填充。你可能已經在提供這個包了。
  • 代碼包2,它幾乎不包含任何轉換和填充,由於它的目標是現代瀏覽器。這是你可能沒有提供的包—至少如今尚未。

實現這一點有點複雜,我寫了一種實現方法,在這裏就不深究了,簡而言之就是,你能夠修改構建的配置來生成一份額外的更小版本的代碼包,而且只提供給現代瀏覽器。最重要的是,這些都是能夠在不犧牲任何特性或功能的狀況下實現的節省。視你的應用程序代碼而定,節省的成本可能會至關可觀。

項目傳統打包(左)與現代包(右)的 webpack-bundle-analyzer 分析

將這些包提供給對應平臺的最簡單模式以下,它在現代瀏覽器中也很好用:

<!-- 現代瀏覽器加載這份文件: -->
<script type="module" src="/js/app.mjs"></script>
<!-- 傳統瀏覽器加載這份文件: -->
<script defer nomodule src="/js/app.js"></script>
複製代碼

不幸的是,這種模式有一個警告:像 IE 11 這樣的傳統瀏覽器,甚至像 Edge 15 到 18 這樣相對現代的瀏覽器,都會同時下載這兩個包。若是這對你來講是能夠接受的,那就沒有問題。

若是你擔憂傳統瀏覽器下載兩組包有性能問題,那麼你須要找一個解決方案。這裏有一個潛在的方案,即便用腳本注入(而不是上面的腳本標籤)來避免在受影響的瀏覽器上重複下載:

var scriptEl = document.createElement("script");

if ("noModule" in scriptEl) {
  // 設置現代腳本
  scriptEl.src = "/js/app.mjs";
  scriptEl.type = "module";
} else {
  // 設置傳統腳本
  scriptEl.src = "/js/app.js";
  scriptEl.defer = true; // type="module" 默認會延遲, 這裏須要手動設置。
}

// 注入!
document.body.appendChild(scriptEl);
複製代碼

這段腳本推斷若是一個瀏覽器在腳本元素中支持 nomodule 屬性,它就能解析 type="module"。這確保了傳統瀏覽器只能加載獲得傳統腳本,而現代瀏覽器只能加載獲得現代腳本。可是須要注意的是,動態注入的腳本默認狀況下是異步加載的,因此若是依賴順序很重要,那麼須要將 async 屬性設置爲 false。

更少的轉換

個人意思並非說要直接廢棄 Bable,它是必不可少的,可是天哪,它在你不知道的狀況下增長了不少額外的東西。檢查一下它轉換的代碼是有好處的。在你的編程習慣上作一些小的改變就會對 Babel 的輸出產生積極的影響。

默認參數是一個很是方便的 ES6 功能,你可能已經使用過:

function logger(message, level = "log") {
  console[level](message);
}
複製代碼

這裏須要注意的是 level 參數,它的默認值是「log」。這意味着若是咱們想用這個函數調用 console.log,咱們不須要指定 level 參數。太好了,對吧?但 Babel 轉換這個函數時,輸出以下:

function logger(message) {
  var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log";

  console[level](message);
}
複製代碼

這是一個例子:儘管咱們的初衷是好的,但開發人員的便利可能會拔苗助長。源代碼中僅有的幾個字節如今已經在生產代碼中轉換爲更大的字節。代碼醜化對此也無能爲力,由於 arguments 沒法壓縮掉。哦,不要認爲 rest 參數可能會是一種更好的解決方案,實際 Babel 將它們轉換得更加龐大:

// 源碼
function logger(...args) {
  const [level, message] = args;

  console[level](message);
}

// Babel 輸出
function logger() {
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
    args[_key] = arguments[_key];
  }

  const level = args[0],
        message = args[1];
  console[level](message);
}
複製代碼

更糟糕的是,Babel 甚至對 @babel/preset-env 配置了針對現代瀏覽器的項目也轉換了這段代碼,這意味差別服務中的 JavaScript 現代包也會受到影響!你可使用 loose transforms 來解決這一漏洞——這是一個好主意,由於它們一般比那些更符合規範的轉換包要小得多——可是,若是你稍後從構建管道中刪除 Babel,啓用 loose transforms 可能會致使問題

不管你決定是否啓用 loose transforms,這裏有一種方法能夠去掉置換的默認參數:

// Babel 不會轉換它
function logger(message, level) {
  console[level || "log"](message);
}
複製代碼

固然,默認參數並非惟一須要警戒的特性。例如,展開語法會被轉換,箭頭函數其它一大堆東西也會被轉換。

若是你不想徹底避免使用這些功能,如下幾個方法能夠減小它們的影響:

  1. 若是你正在編寫一個庫,能夠考慮使用 @babel/runtime 替代 @babel/plugin-transform-runtime ,以防止 Babel 將幫助函數放入你的代碼中。
  2. 對於應用程序中的 polyfilled 功能,能夠經過使用 @babel/preset-env 的 useBuiltIns:「usage」 選項,選擇性地引入 @babel/polyfill

這只是我我的的見解,但我認爲最好的選擇是徹底避免對爲現代瀏覽器生成的包進行代碼轉換。但這不必定可行,若是你使用了 JSX,它就必須針對全部瀏覽器進行轉換,或者若是你使用的是不被普遍支持的前沿語言特性。後一種狀況中,咱們有必要問一下,這些功能對於提供良好的用戶體驗是否真的是必需的(它們不多是必需的)。若是你認爲必定要使用 Babel,那麼你應該時不時地去看看它轉換的內容,看看 Babel 可能會作哪些事情,你是否能夠進行改進。

進步不是一場競賽

當你按摩你的太陽穴,想知道這個可怕的 「JavaScript 宿醉」何時纔會消失,你要知道,正是當咱們急於獲得一些東西的時候,用戶體驗纔會受到影響。因爲 web 開發社區熱衷於以競爭的名義進行更快的迭代,因此你有必要稍微放慢速度。你會發現,這樣作可能會使你的迭代速度不如競爭對手,可是你的產品將比他們的更快。

當你把這些建議應用到你的代碼庫中時,要知道進步不是一晚上之間天然發生的。Web 開發是一項工做。真正有影響力的工做是在咱們深思熟慮並致力於長期的工藝時完成的。專一於穩定的改進,度量、測試、重複,你的站點的用戶體驗將獲得改善,而且隨着時間的推移,你將一點一點地加快速度。


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam

img10.360buyimg.com/wq/jfs/t1/4…

相關文章
相關標籤/搜索