1. 程式人生 > 實用技巧 >LeetCode 406 根據身高重建佇列

LeetCode 406 根據身高重建佇列

大家是從什麼時候接觸效能優化的呢?

第一時間想到的又是什麼呢?雅虎軍規 ? 高效能javascript ?

效能優化沒有標準答案,我們只能不斷地把從搜尋引擎和書中的知識付諸實踐,這個過程是漫長且艱辛的

本文為總結記錄學習修言大佬小冊,感興趣的同學可以購買支援正版

一切從一道面試題開始

從輸入 URL 到頁面載入完成,發生了什麼?

概括來說,分為5步

  1. DNS把url解析成IP
  2. 客戶端通過Ip和服務端建立tcp連線
  3. 客戶端發起http請求
  4. 服務端處理http請求並返回響應
  5. 客戶端拿到響應並渲染頁面

每一步都可以說的很細,這5個過程就是提高前端效能的根本的切入點

關於第一第二步,我們前端能做的非常有限,理解為主,

DNS 解析花時間,能不能儘量減少解析次數或者把解析前置?能——瀏覽器 DNS 快取和 DNS prefetch

TCP 每次的三次握手都急死人,有沒有解決方案?有——長連線、預連線、接入 SPDY 協議

為了知其所以然(應對深究的面試官),我們瞭解下DNS的解析過程:

  1. 瀏覽器先檢查自身快取中有沒有被解析過的這個域名對應的ip地址,如果有,解析結束
  2. 如果瀏覽器快取沒有命中,瀏覽器會檢查作業系統快取中有沒有對應的已解析過的結果。而作業系統也有一個域名解析的過程。在windows中可通過c盤裡一個叫hosts的檔案來設定,如果你在這裡指定了一個域名對應的ip地址,那瀏覽器會首先使用這個ip地址。但是這種作業系統級別的域名解析規程也被很多黑客利用,通過修改你的hosts檔案裡的內容把特定的域名解析到他指定的ip地址上,造成所謂的域名劫持。所以在windows7中將hosts檔案設定成了readonly,防止被惡意篡改。
  3. 如果至此還沒有命中域名,才會真正的請求本地域名伺服器(LDNS)來解析這個域名,這臺伺服器一般在你的城市的某個角落,距離你不會很遠,並且這臺伺服器的效能都很好,一般都會快取域名解析結果,大約80%的域名解析到這裡就完成了。
  4. 如果LDNS仍然沒有命中,就直接跳到Root Server 域名伺服器請求解析
  5. 根域名伺服器返回給LDNS一個所查詢域的主域名伺服器(gTLD Server,國際頂尖域名伺服器,如.com .cn .org等)地址
  6. 此時LDNS再發送請求給上一步返回的gTLD
  7. 接受請求的gTLD查詢並返回這個域名對應的Name Server的地址,這個Name Server就是網站註冊的域名伺服器
  8. Name Server根據對映關係表找到目標ip,返回給LDNS
  9. LDNS快取這個域名和對應的ip
  10. LDNS把解析的結果返回給使用者,使用者根據TTL值快取到本地系統快取中,域名解析過程至此結束

HTTP 連線這一層面的優化才是我們網路優化的核心,

  1. 減少http請求次數
  2. 減少單次請求花費的時間

減少h t tp請求次數就必定會增加單次請求的開銷,兩點怎麼權衡?

網路篇

減少單次請求花費的時間

webpack 效能優化

最常見操作就是資源的壓縮和合並,該操作最常見的工具就是webpack, 所以問題就指向了webpack的效能瓶頸

構建過程時間太長

打包體積太大

從 webpack v4.0.0 開始,可以不用引入一個配置檔案。然而,webpack 仍然還是高度可配置的

不要讓loader做太多事

以 babel-loader 為例:

下面直接貼webpack官網的描述

babel-loader 很慢!

確保轉譯儘可能少的檔案。你可能使用 /\.js$/ 來匹配,這樣也許會去轉譯 node_modules 目錄或者其他不需要的原始碼。

要排除 node_modules,參考文件中的 loaders 配置的 exclude 選項。

你也可以通過使用 cacheDirectory 選項,將 babel-loader 提速至少兩倍。 這會將轉譯的結果快取到檔案系統中。

babel 對一些公共方法使用了非常小的輔助程式碼,比如 _extend。 預設情況下會被新增到每一個需要它的檔案中

你可以引入 babel runtime 作為一個獨立模組,來避免重複引入。

下面的配置禁用了 babel 自動對每個檔案的 runtime 注入,而是引入 babel-plugin-transform-runtime 並且使所有輔助程式碼從這裡引用。

rules: [
  // 'transform-runtime' 外掛告訴 babel 要引用 runtime 來代替注入。
  {
    test: /\.js$/,
    exclude: /(node_modules|bower_components)/,
    use: {
      loader: 'babel-loader?cacheDirectory=true',
      options: {
        presets: ['@babel/preset-env'],
        plugins: ['@babel/transform-runtime']
      }
    }
  }
]

不要放過第三方庫

打包第三方依賴推薦 DllPlugin

DllPlugin 是基於 Windows 動態連結庫(dll)的思想被創作出來的。這個外掛會把第三方庫單獨打包到一個檔案中,這個檔案就是一個單純的依賴庫。這個依賴庫不會跟著你的業務程式碼一起被重新打包,只有當依賴自身發生版本變化時才會重新打包

Dll.config.js

// 以一個基於 React 的簡單專案為例,我們的 dll 的配置檔案可以編寫如下
const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: {
      // 依賴的庫陣列
      vendor: [
        'prop-types',
        'babel-polyfill',
        'react',
        'react-dom',
        'react-router-dom',
      ]
    },
    output: {
      path: path.join(__dirname, 'dist'),
      filename: '[name].js',
      library: '[name]_[hash]',
    },
    plugins: [
      new webpack.DllPlugin({
        // DllPlugin的name屬性需要和libary保持一致
        name: '[name]_[hash]',
        path: path.join(__dirname, 'dist', '[name]-manifest.json'),
        // context需要和webpack.config.js保持一致
        context: __dirname,
      }),
    ],
}

執行這個配置檔案,我們的 dist 資料夾裡會出現這樣兩個檔案:

  1. vendor-manifest.json: 用於描述每個第三方庫對應的具體路徑
  2. vendor.js: 我們第三方庫打包的結果

Webpack.config.js

const path = require('path');
const webpack = require('webpack')
module.exports = {
  mode: 'production',
  // 編譯入口
  entry: {
    main: './src/index.js'
  },
  // 目標檔案
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // dll相關配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // manifest就是我們第一步中打包出來的json檔案
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}

Happypack——將 loader 由單程序轉為多程序

webpack由於是node編寫的,node是單執行緒的,就算此刻存在多個任務,你也只能排隊一個接一個地等待處理, 為了充分利用多核cpu的資源,根據cpu的核數,我們可以fork多個程序, Happypack就是為此而生

const HappyPack = require('happypack')
// 手動建立程序池
const happyThreadPool =  HappyPack.ThreadPool({ size: os.cpus().length })

module.exports = {
  module: {
    rules: [
      ...
      {
        test: /\.js$/,
        // 問號後面的查詢引數指定了處理這類檔案的HappyPack例項的名字
        loader: 'happypack/loader?id=happyBabel',
        ...
      },
    ],
  },
  plugins: [
    ...
    new HappyPack({
      // 這個HappyPack的“名字”就叫做happyBabel,和樓上的查詢引數遙相呼應
      id: 'happyBabel',
      // 指定程序池
      threadPool: happyThreadPool,
      loaders: ['babel-loader?cacheDirectory']
    })
  ],
}

這樣就可以併發處理多個任務

視覺化打包後各個包的體積

webpack-bundle-analyzer

大家可以點進去看一下,它將建立一個互動式treemap視覺化你的包的內容,這樣你就可以知道哪些包是引起你打包體積過大的罪魁禍首

刪除冗餘程式碼

tree shaking

tree shaking 是一個術語,通常用於描述移除 JavaScript 上下文中的未引用程式碼(dead-code)。它依賴於 ES2015 模組系統中的靜態結構特性,例如 importexport。這個術語和概念實際上是興起於 ES2015 模組打包工具 rollup

新的 webpack 4 正式版本,擴充套件了這個檢測能力,通過 package.json"sideEffects" 屬性作為標記,向 compiler 提供提示,表明專案中的哪些檔案是 "pure(純的 ES2015 模組)",由此可以安全地刪除檔案中未使用的部分。

如果所有程式碼都不包含副作用,我們就可以簡單地將該屬性標記為 false,來告知 webpack,它可以安全地刪除未用到的 export 匯出。

{
  "name": "your-project",
  "sideEffects": false
}

如果你的程式碼確實有一些副作用,那麼可以改為提供一個數組:

陣列方式支援相關檔案的相對路徑、絕對路徑和 glob 模式。它在內部使用 micromatch

注意,任何匯入的檔案都會受到 tree shaking 的影響。這意味著,如果在專案中使用類似 css-loader 並匯入 CSS 檔案,則需要將其新增到 side effect 列表中,以免在生產模式中無意中將它刪除:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js"
    "*.css"
  ]
}

Tree-Shaking 的針對性很強,它更適合用來處理模組級別的冗餘程式碼

UglifyJsPlugin

// 在壓縮過程中對碎片化的冗餘程式碼(如 console 語句、註釋等)進行自動化刪除
const webpack = require('webpack');
module.exports = {
 plugins: [
   new webpack.optimize.UglifyJsPlugin({
   		// 允許併發
     parallel: true,
     // 開啟快取
     cache: true,
     compress: {
       // 刪除所有的console語句    
       drop_console: true,
       // 把使用多次的靜態值自動定義為變數
       reduce_vars: true,
     },
     output: {
       // 不保留註釋
       comment: false,
       // 使輸出的程式碼儘可能緊湊
       beautify: false
     }
  }),
 ]
}

按需載入

比如路由的懶載入,tab元件的懶載入

Gzip

gzip的基礎是DEFLATE,DEFLATE是LZ77與哈夫曼編碼的一個組合體。DEFLATE最初是作為LZW以及其它受專利保護的資料壓縮演算法的替代版本而設計的,當時那些專利限制了compress以及其它一些流行的歸檔工具的應用

開啟gzip壓縮,只需要在請求頭中加上

accept-encoding:gzip

壓縮後通常能幫我們減少響應 70% 左右的大小

Gzip 壓縮背後的原理,是在一個文字檔案中找出一些重複出現的字串、臨時替換它們,從而使整個檔案變小。根據這個原理,檔案中程式碼的重複率越高,那麼壓縮的效率就越高,使用 Gzip 的收益也就越大。反之亦然

圖片優化

圖片優化就是在圖片體積和圖片質量之間做權衡

前置知識: 在計算機中,畫素用二進位制數來表示, n位二進位制可以表示2^n種顏色,

所以二進位制位數越多,可表示的顏色種類就越多,成像效果越細膩,圖片體積越大

我們需要在不同的業務場景選擇合適的圖片型別

JPEG/JPG

體積小有失真壓縮不支援透明載入快

當我們把圖片體積壓縮至原有體積的 50% 以下時,JPG 仍然可以保持住 60% 的品質。此外,JPG 格式以 24 位儲存單個圖,可以呈現多達2^24 = 16,777,216 種顏色,足以應對大多數場景下對色彩的要求

使用場景

JPG 圖片經常作為大的背景圖、輪播圖或 Banner 圖出現,使用 JPG 呈現大圖,既可以保住圖片的質量,又不會帶來令人頭疼的圖片體積,是當下比較推崇的一種方案。

PNG-8/PNG-24

體積大無失真壓縮支援透明質量高

8和24代表2進位制位數

Png-8: 2^8 = 256中顏色

Png-24: 2^24 = 16,777,216 種顏色

追求極致的顯示效果,不在意圖片大小的可以選擇png-24

實踐中,如果png-8沒有帶來視覺可辨別的色彩缺陷,考慮到體積,一般使用png-8

使用場景

考慮到 PNG 在處理線條和顏色對比度方面的優勢,我們主要用它來呈現小的 Logo、顏色簡單且對比強烈的圖片或背景等。

效能方面堪稱業界楷模的淘寶首頁頁面上的 Logo,無論大小,都是 PNG 格式

SVG

體積小不失真文字檔案相容性好

SVG(可縮放向量圖形)是一種基於 XML 語法的影象格式。它和本文提及的其它圖片種類有著本質的不同:SVG 對影象的處理不是基於畫素點,而是是基於對影象的形狀描述。

優勢

  • SVG 與 PNG 和 JPG 相比,檔案體積更小,可壓縮性更強
  • 作為向量圖,它最顯著的優勢在於圖片可無限放大而不失真, 這使得 SVG 即使是被放到視網膜螢幕上,也可以一如既往地展現出較好的成像品質——1 張 SVG 足以適配 n 種解析度
  • SVG 是文字檔案。我們既可以像寫程式碼一樣定義 SVG,把它寫在 HTML 裡、成為 DOM 的一部分,也可以把對圖形的描述寫入以 .svg 為字尾的獨立檔案, 這使得 SVG 檔案可以被非常多的工具讀取和修改,具有較強的靈活性

劣勢

  • 渲染成本高
  • 相比其他圖片格式,學習成本高,因為它是可程式設計的

使用場景

小圖示

Base64

文字檔案依賴編碼小圖示解決方案

Base64 並非一種圖片格式,而是一種編碼方式。Base64 和雪碧圖一樣,是作為小圖示解決方案而存在的。在瞭解 Base64 之前,我們先來了解一下雪碧圖

雪碧圖

影象精靈(sprite,意為精靈),被運用於眾多使用大量小圖示的網頁應用之上。它可取影象的一部分來使用,使得使用一個影象檔案替代多個小檔案成為可能。相較於一個小圖示一個影象檔案,單獨一張圖片所需的 HTTP 請求更少,對記憶體和頻寬更加友好。

前端使用background-position 來獲取不同位置的圖示

Base64 圖片的出現,也是為了減少載入網頁圖片時對伺服器的請求次數,從而提升網頁效能。Base64 是作為雪碧圖的補充而存在的。

使用場景

小logo, 小icon

  • 圖片的實際尺寸很小(大家可以觀察一下掘金頁面的 Base64 圖,幾乎沒有超過 2kb 的)
  • 圖片無法以雪碧圖的形式與其它小圖結合(合成雪碧圖仍是主要的減少 HTTP 請求的途徑,Base64 是雪碧圖的補充)
  • 圖片的更新頻率非常低(不需我們重複編碼和修改檔案內容,維護成本較低)

劣勢

Base64 編碼後,圖片大小會膨脹為原檔案的 4/3(這是由 Base64 的編碼原理決定的),

如果我們把大圖也編碼到 HTML 或 CSS 檔案中,後者的體積會明顯增加,即便我們減少了 HTTP 請求,也無法彌補這龐大的體積帶來的效能開銷,得不償失

base64編碼工具推薦

webpack 的 url-loader , limit引數表示 指定檔案的最大大小,以位元組為單位,只有8192byte一下大小的圖片才會進行base64編碼

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          },
        ],
      },
    ],
  },
};

WebP

年輕的全能型選手

它於 2010 年被提出, 是 Google 專為 Web 開發的一種旨在加快圖片載入速度的圖片格式,它支援有失真壓縮和無失真壓縮

與 PNG 相比,WebP 無損影象的尺寸縮小了 26%。在等效的 SSIM 質量指數下,WebP 有損影象比同類 JPEG 影象小 25-34%。

  • 最大的缺陷就是相容性
  • WebP 還會增加伺服器的負擔——和編碼 JPG 檔案相比,編碼同樣質量的 WebP 檔案會佔用更多的計算資源

應用場景

要使用webP我們就必須為不相容的瀏覽器進行降級處理

我們可以看看淘寶是怎麼做的

在谷歌瀏覽器開啟淘寶,開啟控制檯,搜尋.webp

其中一個img的src是這樣的

img.alicdn.com/imgextra/i3/6000000002336/O1CN019A9rll1T7vqVs4Wur_!!6000000002336-0-octopus.jpg_400x400q90.jpg_.webp

在safari中開啟淘寶,開啟控制檯,檢視同一張圖片的src

img.alicdn.com/imgextra/i3/6000000002336/O1CN019A9rll1T7vqVs4Wur_!!6000000002336-0-octopus.jpg_400x400q90.jpg

淘寶是會根據瀏覽器的型號來判斷是否支援webp,不支援就把.webp字尾切換成.jpg

更靈活的方案

把判斷邏輯交給後端,伺服器根據 請求頭的 Accept 欄位 來判斷是否支援webp, 否則返回原圖(jpg),

這樣做的好處是當webp的相容性發生變化時,前端不用修改判斷是否支援webp的程式碼

儲存篇

通過網路獲取內容既速度緩慢又開銷巨大。較大的響應需要在客戶端與伺服器之間進行多次往返通訊,這會延遲瀏覽器獲得和處理內容的時間,還會增加訪問者的流量費用。因此,快取並重複利用之前獲取的資源的能力成為效能優化的一個關鍵方面。

瀏覽器快取機制

按照獲取資源時請求的優先順序排序:

  1. Memory Cache
  2. Service Worker Cache
  3. HTTP Cache
  4. Push Cache

Memory Cache(記憶體快取)

是指存在記憶體中的快取。從優先順序上來說,它是瀏覽器最先嚐試去命中的一種快取。從效率上來說,它是響應速度最快的一種快取。

記憶體快取是快的,也是“短命”的。它和渲染程序“生死相依”,當程序結束後,也就是 tab 關閉以後,記憶體裡的資料也將不復存在

資源存不存記憶體,瀏覽器秉承的是“節約原則”。我們發現,Base64 格式的圖片,幾乎永遠可以被塞進 memory cache,這可以視作瀏覽器為節省渲染開銷的“自保行為”;此外,體積不大的 JS、CSS 檔案,也有較大地被寫入記憶體的機率——相比之下,較大的 JS、CSS 檔案就沒有這個待遇了,記憶體資源是有限的,它們往往被直接甩進磁碟

Service Worker Cache

Service Worker 是一種獨立於主執行緒之外的 Javascript 執行緒。它脫離於瀏覽器窗體,因此無法直接訪問 DOM。這樣獨立的個性使得 Service Worker 的“個人行為”無法干擾頁面的效能,這個“幕後工作者”可以幫我們實現離線快取訊息推送網路代理等功能。我們藉助 Service worker 實現的離線快取就稱為 Service Worker Cache。

Service Worker 的生命週期包括 installactiveworking三個階段。一旦 Service Worker 被 install,它將始終存在,只會在 active 與 working 之間切換,除非我們主動終止它。這是它可以用來實現離線儲存的重要先決條件。

如何使用 service worker(必須以 https 協議為前提)

新建test.js檔案

// Service Worker會監聽 install事件,我們在其對應的回撥裡可以實現初始化的邏輯  
self.addEventListener('install', event => {
  event.waitUntil(
    // 考慮到快取也需要更新,open內傳入的引數為快取的版本號
    // caches 為瀏覽器的API
    // CacheStorage {}
		//   __proto__: CacheStorage
		//   delete: ƒ delete()
		//  	has: ƒ has()
    //   keys: ƒ keys()
    //    match: ƒ match()
    //    open: ƒ open()
    //    constructor: ƒ CacheStorage()
    //   Symbol(Symbol.toStringTag): "CacheStorage"
    //   __proto__: Object
    caches.open('test-v1').then(cache => {
      return cache.addAll([
        // 此處傳入指定的需快取的檔名
        '/test.html',
        '/test.css',
        '/test.js'
      ])
    })
  )
})

// Service Worker會監聽所有的網路請求,網路請求的產生觸發的是fetch事件,我們可以在其對應的監聽函式中實現對請求的攔截,進而判斷是否有對應到該請求的快取,實現從Service Worker中取到快取的目的
self.addEventListener('fetch', event => {
  event.respondWith(
    // 嘗試匹配該請求對應的快取值
    caches.match(event.request).then(res => {
      // 如果匹配到了,呼叫Server Worker快取
      if (res) {
        return res;
      }
      // 如果沒匹配到,向服務端發起這個資源請求
      return fetch(event.request).then(response => {
        if (!response || response.status !== 200) {
          return response;
        }
        // 請求成功的話,將請求快取起來。
        caches.open('test-v1').then(function(cache) {
          cache.put(event.request, response);
        });
        return response.clone();
      });
    })
  );
});

在專案程式碼入口j s檔案中加入

window.navigator.serviceWorker.register('/test.js').then(
   function () {
      console.log('註冊成功')
    }).catch(err => {
      console.error("註冊失敗")
    })

HTTP Cache(重點)

HTTP 快取是我們日常開發中最為熟悉的一種快取機制。它又分為強快取協商快取。優先順序較高的是強快取,在命中強快取失敗的情況下,才會走協商快取。

強快取

強快取是利用 http 頭中的 ExpiresCache-Control兩個欄位來控制的。強快取中,當請求再次發出時,瀏覽器會根據其中的 expires 和 cache-control 判斷目標資源是否“命中”強快取,若命中則直接從快取中獲取資源,不會再與服務端發生通訊。

命中強快取的情況下,返回的 HTTP 狀態碼為200 (from disk cache)

Cache-Control 的 max-age 配置項相對於 expires 的優先順序更高。當 Cache-Control 與 expires 同時出現時,我們以 Cache-Control 為準。

Cache-Control
cache-control: max-age=31536000

max-age表示的有效時長, 單位是秒,表示該資源31536000秒內是有效的

public 與 private

public 與 private 是針對資源是否能夠被代理服務快取而存在的一組對立概念。

如果我們為資源設定了 public,那麼它既可以被瀏覽器快取,也可以被代理伺服器快取;如果我們設定了 private,則該資源只能被瀏覽器快取。private 為預設值。但多數情況下,public 並不需要我們手動設定

no-store與no-cache

no-cache 繞開了瀏覽器:我們為資源設定了 no-cache 後,每一次發起請求都不會再去詢問瀏覽器的快取情況,而是直接向服務端去確認該資源是否過期(不走強快取)。

no-store 比較絕情,顧名思義就是不使用任何快取策略。在 no-cache 的基礎上,它連服務端的快取確認也繞開了,只允許你直接向服務端傳送請求、並下載完整的響應。

Expires
expires: Wed, 11 Sep 2019 16:12:18 GMT

expires 設定的是資源到期時間(伺服器時間)

接下來如果我們試圖再次向伺服器請求資源,瀏覽器就會先對比本地時間和 expires 的時間戳,如果本地時間小於 expires 設定的過期時間,那麼就直接去快取中取這個資源。由於時間戳是伺服器來定義的,而本地時間的取值卻來自客戶端,因此 expires 的工作機制對客戶端時間與伺服器時間之間的一致性提出了極高的要求,若伺服器與客戶端存在時差,將帶來意料之外的結果。

協商快取

協商快取機制下,瀏覽器需要向伺服器去詢問快取的相關資訊,進而判斷是重新發起請求、下載完整的響應,還是從本地獲取快取的資源。

如果服務端提示快取資源未改動(Not Modified),資源會被重定向到瀏覽器快取,這種情況下網路請求對應的狀態碼是 304

Etag 在感知檔案變化上比 Last-Modified 更加準確,優先順序也更高。當 Etag 和 Last-Modified 同時存在時,以 Etag 為準.

Last-Modified

首次請求時,響應頭裡會攜帶Last-Modified欄位,像這樣

Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT

再次請求相同的資源,請求頭裡會帶上If-Modified-Since欄位,

值是上一次請求響應頭的Last-Modified的值

If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT

伺服器接收到這個時間戳後,會比對該時間戳和資源在伺服器上的最後修改時間是否一致,從而判斷資源是否發生了變化。如果發生了變化,就會返回一個完整的響應內容,並在 響應頭 中新增新的 Last-Modified 值;否則,返回如上圖的 304 響應,響應頭 不會再新增 Last-Modified 欄位。

弊端

  • 我們編輯了檔案,但檔案的內容沒有改變。服務端並不清楚我們是否真正改變了檔案,它仍然通過最後編輯時間進行判斷。因此這個資源在再次被請求時,會被當做新資源,進而引發一次完整的響應——不該重新請求的時候,也會重新請求
  • 當我們修改檔案的速度過快時(比如花了 100ms 完成了改動),由於 If-Modified-Since 只能檢查到以秒為最小計量單位的時間差,所以它是感知不到這個改動的——該重新請求的時候,反而沒有重新請求了

為了解決這樣的問題,Etag 作為 Last-Modified 的補充出現了

Etag

Etag 是由伺服器為每個資源生成的唯一的標識字串,這個標識字串是基於檔案內容編碼的,只要檔案內容不同,它們對應的 Etag 就是不同的,反之亦然。因此 Etag 能夠精準地感知檔案的變化。

首次請求時,響應頭裡會攜帶ETag欄位,像這樣

ETag: W/"2a3b-1602480f459"

再次請求相同的資源,請求頭裡會帶上If-Modified-Since欄位,

值是上一次請求響應頭的ETag的值

If-None-Match: W/"2a3b-1602480f459"

伺服器會拿If-None-Match的值和伺服器資源的當前標識對比,相同返回304, 不同返回新的資源並帶上新的Etag

Etag 的生成過程需要伺服器額外付出開銷,會影響服務端的效能,這是它的弊端。因此啟用 Etag 需要我們審時度勢。正如我們剛剛所提到的——Etag 並不能替代 Last-Modified,它只能作為 Last-Modified 的補充和強化存在。

本地儲存

Cookie 的本職工作並非本地儲存,而是“維持狀態”。

在 Web 開發的早期,人們亟需解決的一個問題就是狀態管理的問題:HTTP 協議是一個無狀態協議,伺服器接收客戶端的請求,返回一個響應,故事到此就結束了,伺服器並沒有記錄下關於客戶端的任何資訊。那麼下次請求的時候,如何讓伺服器知道“我是我”呢?

在這樣的背景下,Cookie 應運而生。
學習中,持續更新。。。