Uzun süreli önbelleğe alma özelliğinden yararlanın

Webpack, öğeleri önbelleğe almaya nasıl yardımcı olur?

Uygulama boyutunu optimize ettikten sonra uygulamanın yüklenme süresini kısaltan bir sonraki işlem, önbelleğe almadır. Uygulamanın bazı bölümlerini istemcide tutmak ve her defasında tekrar indirmekten kaçınmak için bunu kullanın.

Paket sürümü oluşturmayı ve önbellek üstbilgilerini kullanma

Önbelleğe alma işleminde genel yaklaşım şu şekildedir:

  1. tarayıcıya dosyayı çok uzun bir süre (ör. bir yıl) önbelleğe almasını bildirin:

    # Server header
    Cache-Control: max-age=31536000
    

    Cache-Control ürününün ne işe yaradığını bilmiyorsanız Jake Archibald'ın en iyi önbelleğe alma uygulamalarını ele alan mükemmel yayınını inceleyin.

  2. ve yeniden indirmeyi zorunlu kılmak için değiştirildiğinde dosyayı yeniden adlandırın:

    <!-- Before the change -->
    <script src="./index-v15.js"></script>
    
    <!-- After the change -->
    <script src="./index-v16.js"></script>
    

Bu yaklaşım, tarayıcıya JS dosyasını indirmesini, önbelleğe almasını ve önbelleğe alınan kopyayı kullanmasını söyler. Tarayıcı yalnızca dosya adı değişirse (veya bir yıl geçerse) ağa çalışır.

Webpack ile de aynısını yaparsınız ancak sürüm numarası yerine dosya karmasını belirtirsiniz. Dosya adına karma değerini eklemek için [chunkhash] işlevini kullanın:

// webpack.config.js
module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.[chunkhash].js' // → bundle.8e0d62a03.js
  }
};

İstemciye göndermek için dosya adına ihtiyacınız varsa HtmlWebpackPlugin veya WebpackManifestPlugin özelliğini kullanın.

HtmlWebpackPlugin basit ancak daha az esnek bir yaklaşımdır. Bu eklenti, derleme sırasında derlenen tüm kaynakları içeren bir HTML dosyası oluşturur. Sunucu mantığınız karmaşık değilse sizin için yeterli olacaktır:

<!-- index.html -->
<!DOCTYPE html>
<!-- ... -->
<script src="bundle.8e0d62a03.js"></script>

WebpackManifestPlugin, karmaşık bir sunucu parçanız varsa kullanışlı olan daha esnek bir yaklaşımdır. Derleme sırasında, karma olmayan dosya adları ile karma içeren dosya adları arasında eşleme içeren bir JSON dosyası oluşturur. Hangi dosyayla çalışacağınızı bulmak için sunucuda bu JSON'u kullanın:

// manifest.json
{
  "bundle.js": "bundle.8e0d62a03.js"
}

Daha fazla bilgi

Bağımlılıkları ve çalışma zamanını ayrı bir dosyaya ayıklayın

Bağımlılıklar

Uygulama bağımlılıkları genellikle gerçek uygulama koduna göre daha az değişme eğilimindedir. Bunları ayrı bir dosyaya taşırsanız, tarayıcı bunları ayrı olarak önbelleğe alabilir ve uygulama kodu her değiştiğinde bunları yeniden indirmez.

Bağımlılıkları ayrı bir parçaya ayıklamak için üç adım uygulayın:

  1. Çıkış dosya adını [name].[chunkname].js ile değiştirin:

    // webpack.config.js
    module.exports = {
      output: {
        // Before
        filename: 'bundle.[chunkhash].js',
        // After
        filename: '[name].[chunkhash].js'
      }
    };
    

    Webpack, uygulamayı derlerken [name] öğesini bir parçanın adıyla değiştirir. [name] bölümünü eklemezsek parçaları karmalarına göre ayırt etmemiz gerekecek. Bu oldukça zor bir iş.

  2. entry alanını bir nesneye dönüştürün:

    // webpack.config.js
    module.exports = {
      // Before
      entry: './index.js',
      // After
      entry: {
        main: './index.js'
      }
    };
    

    Bu snippet'te "main", bir yığının adıdır. Bu ad, 1. adımdaki [name] yerine değiştirilecektir.

    Uygulamayı derlerseniz bu parça şu ana kadar uygulama kodunun tamamını içerir (tıpkı bu adımları uygulamadığımız gibi). Ancak bu durum bir saniye içinde değişecek.

  3. webpack 4'te web paketi yapılandırmanıza optimization.splitChunks.chunks: 'all' seçeneğini ekleyin:

    // webpack.config.js (for webpack 4)
    module.exports = {
      optimization: {
        splitChunks: {
          chunks: 'all'
        }
      }
    };
    

    Bu seçenek, akıllı kod bölmeyi etkinleştirir. Bununla birlikte webpack, 30 kB'tan büyük olursa (küçültme ve gzip'ten önce) tedarikçi kodunu ayıklar. Ortak kodu da ayıklar.Bu, derlemeniz birkaç paket oluşturuyorsa (ör. uygulamanızı rotalara bölerseniz) kullanışlıdır.

    webpack 3'te CommonsChunkPlugin öğesini ekleyin:

    // webpack.config.js (for webpack 3)
    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
        // A name of the chunk that will include the dependencies.
        // This name is substituted in place of [name] from step 1
        name: 'vendor',
    
        // A function that determines which modules to include into this chunk
        minChunks: module => module.context && module.context.includes('node_modules'),
        })
      ]
    };
    

    Bu eklenti, yolları node_modules içeren tüm modülleri alır ve vendor.[chunkhash].js adlı ayrı bir dosyaya taşır.

Bu değişikliklerden sonra, her derleme bir yerine iki dosya oluşturacak: main.[chunkhash].js ve vendor.[chunkhash].js (webpack 4 için vendors~main.[chunkhash].js). Webpack 4'te, bağımlılıklar küçükse tedarikçi paketi oluşturulmayabilir. Sorun değil:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                        Asset      Size  Chunks             Chunk Names
 ./main.00bab6fd3100008a42b0.js   82 kB       0  [emitted]  main
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

Tarayıcı bu dosyaları ayrı olarak önbelleğe alır ve yalnızca değişen kodu yeniden indirir.

Web paketi çalışma zamanı kodu

Maalesef yalnızca satıcının kodunu almak yeterli değildir. Uygulama kodunda bir şeyi değiştirmeye çalışırsanız:

// index.js
…
…

// E.g. add this:
console.log('Wat');

vendor karmasının da değiştiğini fark edeceksiniz:

                           Asset   Size  Chunks             Chunk Names
./vendor.d9e134771799ecdf9483.js  47 kB       1  [emitted]  vendor

                            Asset   Size  Chunks             Chunk Names
./vendor.e6ea4504d61a1cc1c60b.js  47 kB       1  [emitted]  vendor

Bunun nedeni, modül kodundan ayrı olarak web paketi paketinde, modülün yürütme işlemini yöneten küçük bir kod parçası olan bir çalışma zamanının bulunmasıdır. Kodu birden fazla dosyaya böldüğünüzde, bu kod parçası parça kimlikleri ve karşılık gelen dosyalar arasında bir eşleme oluşturmaya başlar:

// vendor.e6ea4504d61a1cc1c60b.js
script.src = __webpack_require__.p + chunkId + "." + {
    "0": "2f2269c7f0a55a5c1871"
}[chunkId] + ".js";

Webpack, bu çalışma zamanını bizim örneğimizde vendor olan son oluşturulan yığına ekler. Herhangi bir parça her değiştiğinde bu kod parçası da değişir ve tüm vendor parçasının değişmesine neden olur.

Bu sorunu çözmek için, çalışma zamanını ayrı bir dosyaya taşıyalım. webpack 4'te bu,optimization.runtimeChunk seçeneği etkinleştirilerek gerçekleştirilir:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    runtimeChunk: true
  }
};

webpack 3'te bunu,CommonsChunkPlugin ile fazladan boş bir yığın oluşturarak yapın:

// webpack.config.js (for webpack 3)
module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: module => module.context && module.context.includes('node_modules')
    }),
    // This plugin must come after the vendor one (because webpack
    // includes runtime into the last chunk)
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime',
      // minChunks: Infinity means that no app modules
      // will be included into this chunk
      minChunks: Infinity
    })
  ]
};

Bu değişikliklerden sonra her derleme üç dosya oluşturacaktır:

$ webpack
Hash: ac01483e8fec1fa70676
Version: webpack 3.8.1
Time: 3816ms
                            Asset     Size  Chunks             Chunk Names
   ./main.00bab6fd3100008a42b0.js    82 kB       0  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       1  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

Bunları ters sırada index.html içine ekleyin. Hepsi bu kadar.

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>
<script src="./vendor.26886caf15818fa82dfa.js"></script>
<script src="./main.00bab6fd3100008a42b0.js"></script>

Daha fazla bilgi

Ekstra HTTP isteği kaydetmek için satır içi web paketi çalışma zamanı

İşleri daha da iyileştirmek için web paketi çalışma zamanını HTML yanıtına satır içine almayı deneyin. Örneğin, şunun yerine:

<!-- index.html -->
<script src="./runtime.79f17c27b335abc7aaf4.js"></script>

şunu yap:

<!-- index.html -->
<script>
!function(e){function n(r){if(t[r])return t[r].exports;…}} ([]);
</script>

Çalışma zamanı küçüktür ve bunu satır içine almak, bir HTTP isteğini kaydetmenize yardımcı olur (HTTP/1 için oldukça önemlidir; HTTP/2'de daha az önemlidir ancak yine de bir efekt oynayabilir).

Bunu şu şekilde yapabilirsiniz:

HTMLWebpackEklentileri ile oluşturuyorsanız

HTML dosyası oluşturmak için HtmlWebpackPlugin kullanırsanız ihtiyacınız olan tek şey InlineSourcePlugin'dir:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineSourcePlugin = require('html-webpack-inline-source-plugin');

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      inlineSource: 'runtime~.+\\.js',
    }),
    new InlineSourcePlugin()
  ]
};

Özel bir sunucu mantığı kullanarak HTML oluşturursanız

webpack 4 ile:

  1. Çalışma zamanı parçasının oluşturulan adını öğrenmek için WebpackManifestPlugin kodunu ekleyin:

    // webpack.config.js (for webpack 4)
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
      plugins: [
        new ManifestPlugin()
      ]
    };
    

    Bu eklentiyle yapılan derleme, aşağıdaki gibi bir dosya oluşturur:

    // manifest.json
    {
      "runtime~main.js": "runtime~main.8e0d62a03.js"
    }
    
  2. Çalışma zamanı yığınının içeriğini uygun bir şekilde satır içine alın. Ör. Node.js ve Express ile:

    // server.js
    const fs = require('fs');
    const manifest = require('./manifest.json');
    const runtimeContent = fs.readFileSync(manifest['runtime~main.js'], 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Veya webpack 3 ile:

  1. filename belirterek çalışma zamanı adını statik yapın:

    module.exports = {
      plugins: [
        new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          minChunks: Infinity,
          filename: 'runtime.js'
        })
      ]
    };
    
  2. runtime.js içeriğini uygun bir şekilde satır içine alın. Ör. Node.js ve Express ile:

    // server.js
    const fs = require('fs');
    const runtimeContent = fs.readFileSync('./runtime.js', 'utf-8');
    
    app.get('/', (req, res) => {
      res.send(`
        …
        <script>${runtimeContent}</script>
        …
      `);
    });
    

Şu anda ihtiyaç duymadığınız geç yükleme kodu

Bazen bir sayfada daha fazla veya daha az önemli bölümler olabilir:

  • YouTube'da bir video sayfası yüklerseniz yorumlardan çok videoya önem verirsiniz. Bu açıdan video yorumlardan daha önemli.
  • Bir haber sitesinde bir makaleyi açtığınızda reklamlardan çok makalenin metnine önem verirsiniz. Burada metin, reklamlardan daha önemlidir.

Bu gibi durumlarda, önce yalnızca en önemli öğeleri indirip kalan bölümleri daha sonra geç yükleyerek ilk yükleme performansını iyileştirin. Bunun için import() işlevini ve kod bölme özelliğini kullanın:

// videoPlayer.js
export function renderVideoPlayer() { … }

// comments.js
export function renderComments() { … }

// index.js
import {renderVideoPlayer} from './videoPlayer';
renderVideoPlayer();

// …Custom event listener
onShowCommentsClick(() => {
  import('./comments').then((comments) => {
    comments.renderComments();
  });
});

import(), belirli bir modülü dinamik olarak yüklemek istediğinizi belirtir. Web paketi import('./module.js') ifadesini gördüğünde bu modülü ayrı bir parçaya taşır:

$ webpack
Hash: 39b2a53cb4e73f0dc5b2
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.f7e53d8e13e9a2745d6d.js    60 kB       1  [emitted]  main
 ./vendor.4f14b6326a80f4752a98.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

ve bunu yalnızca yürütme import() işlevine ulaştığında indirir.

Bu, main paketini küçülterek ilk yükleme süresini iyileştirir. Daha da önemlisi, önbelleğe almayı iyileştirir. Ana parçadaki kodu değiştirirseniz yorum parçası etkilenmez.

Daha fazla bilgi

Kodu rotalara ve sayfalara böl

Uygulamanızda birden fazla rota veya sayfa olmasına rağmen kodu içeren yalnızca bir JS dosyası (tek bir main parçası) varsa muhtemelen her istekte ekstra bayt sunuyor olabilirsiniz. Örneğin, bir kullanıcı sitenizin ana sayfasını ziyaret ettiğinde:

WebFundamentals ana sayfası

farklı bir sayfadaki makaleyi oluşturmak için kodu yüklemeleri gerekmez, ancak dosyayı yüklerler. Dahası, kullanıcı her zaman yalnızca ana sayfayı ziyaret ederse ve makale kodunda bir değişiklik yaparsanız, webpack, paketin tamamını geçersiz kılar ve kullanıcının uygulamanın tamamını yeniden indirmesi gerekir.

Uygulamayı sayfalara (veya tek sayfalık bir uygulamaysa rotalara) ayırırsak kullanıcı yalnızca alakalı kodu indirir. Ayrıca tarayıcı, uygulama kodunu daha iyi önbelleğe alır: Ana sayfa kodunu değiştirirseniz webpack yalnızca karşılık gelen yığını geçersiz kılar.

Tek sayfalık uygulamalar için

Tek sayfalık uygulamaları rotalara göre bölmek için import() kullanın ("Şu anda ihtiyaç duymadığınız geç yüklenen kod" bölümüne bakın). Bir çerçeve kullanırsanız bunun için bir çözüm mevcut olabilir:

Geleneksel çok sayfalı uygulamalar için

Geleneksel uygulamaları sayfalara göre bölmek için webpack'in giriş noktalarını kullanın. Uygulamanızın üç tür sayfası varsa: ana sayfa, makale sayfası ve kullanıcı hesabı sayfası. Bu sayfada üç giriş olmalıdır:

// webpack.config.js
module.exports = {
  entry: {
    home: './src/Home/index.js',
    article: './src/Article/index.js',
    profile: './src/Profile/index.js'
  }
};

Her giriş dosyası için webpack ayrı bir bağımlılık ağacı oluşturur ve yalnızca bu giriş tarafından kullanılan modülleri içeren bir paket oluşturur:

$ webpack
Hash: 318d7b8490a7382bf23b
Version: webpack 3.8.1
Time: 4273ms
                            Asset     Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./home.91b9ed27366fe7e33d6a.js    18 kB       1  [emitted]  home
./article.87a128755b16ac3294fd.js    32 kB       2  [emitted]  article
./profile.de945dc02685f6166781.js    24 kB       3  [emitted]  profile
 ./vendor.4f14b6326a80f4752a98.js    46 kB       4  [emitted]  vendor
./runtime.318d7b8490a7382bf23b.js  1.45 kB       5  [emitted]  runtime

Dolayısıyla, Lodash yalnızca makale sayfasında kullanılıyorsa home ve profile paketlerinde bu özellik bulunmaz ve kullanıcının ana sayfayı ziyaret ederken bu kitaplığı indirmesi gerekmez.

Ancak, ayrı bağımlılık ağaçlarının dezavantajları vardır. İki giriş noktası Lodash'i kullanıyorsa ve bağımlılıklarınızı bir tedarikçi firma paketine taşımadıysanız her iki giriş noktasında da Lodash'in bir kopyası bulunur. Bu sorunu çözmek için webpack 4'te web paketi yapılandırmanıza optimization.splitChunks.chunks: 'all' seçeneğini ekleyin:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

Bu seçenek, akıllı kod bölmeyi etkinleştirir. Bu seçenek kullanıldığında, webpack otomatik olarak ortak kodu arar ve ayrı dosyalara çıkarır.

Alternatif olarak webpack 3'te CommonsChunkPlugin öğesini kullanabilirsiniz. Bu işlem, genel bağımlılıkları belirtilen yeni bir dosyaya taşır:

module.exports = {
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common',
      minChunks: 2    // 2 is the default value
    })
  ]
};

En iyi değeri bulmak için minChunks değerini kullanabilirsiniz. Genel olarak, küçük tutmak istersiniz ama parça sayısı büyüdükçe artar. Örneğin, 3 parça için minChunks 2 olabilir ancak 30 parça için 8 olabilir. Çünkü parçayı 2'de tutarsanız çok fazla sayıda modül ortak dosyaya girer ve bu değeri çok fazla şişirir.

Daha fazla bilgi

Modül kimliklerini daha kararlı hale getirme

Kodu oluştururken, webpack her modüle bir kimlik atar. Daha sonra bu kimlikler, paketin içindeki require() içinde kullanılır. Kimlikleri genellikle derleme çıktısında modül yollarının hemen öncesinde görürsünüz:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.8ecaf182f5c85b7a8199.js  22.5 kB       0  [emitted]
   ./main.4e50a16675574df6a9e9.js    60 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime

↓ Burada

[0] ./index.js 29 kB {1} [built]
[2] (webpack)/buildin/global.js 488 bytes {2} [built]
[3] (webpack)/buildin/module.js 495 bytes {2} [built]
[4] ./comments.js 58 kB {0} [built]
[5] ./ads.js 74 kB {1} [built]
+ 1 hidden module

Varsayılan olarak, kimlikler bir sayaç kullanılarak hesaplanır (yani ilk modülün kimliği 0, ikincisinin kimliği 1 ve bu şekilde devam eder). Buradaki sorun, yeni bir modül eklediğinizde modül listesinin ortasında görüntülenerek sonraki tüm modüllerin kimliklerini değiştirebilmesidir:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.5c82c0f337fcb22672b5.js    22 kB       0  [emitted]
   ./main.0c8b617dfc40c2827ae3.js    82 kB       1  [emitted]  main
 ./vendor.26886caf15818fa82dfa.js    46 kB       2  [emitted]  vendor
./runtime.79f17c27b335abc7aaf4.js  1.45 kB       3  [emitted]  runtime
   [0] ./index.js 29 kB {1} [built]
   [2] (webpack)/buildin/global.js 488 bytes {2} [built]
   [3] (webpack)/buildin/module.js 495 bytes {2} [built]

↓ Yeni bir modül ekledik...

[4] ./webPlayer.js 24 kB {1} [built]

↓ Ne yaptığına bakın! comments.js artık 4 yerine 5 kimliğine sahip

[5] ./comments.js 58 kB {0} [built]

ads.js artık 5 yerine 6'ya sahip

[6] ./ads.js 74 kB {1} [built]
       + 1 hidden module

Bu işlem, değişen kimliklere sahip modüller içeren veya bunlara bağlı olan tüm parçaları, gerçek kodları değişmemiş olsa bile geçersiz kılar. Örneğimizde, 0 parçası (comments.js içeren parça) ve main parçası (diğer uygulama kodunun bulunduğu parça) geçersiz kılınır. Öte yandan, yalnızca main parçası olması gerekirdi.

Bu sorunu çözmek için HashedModuleIdsPlugin aracılığıyla modül kimliklerinin hesaplanma şeklini değiştirin. Sayaca dayalı kimlikleri, modül yollarının karmalarıyla değiştirir:

$ webpack
Hash: df3474e4f76528e3bbc9
Version: webpack 3.8.1
Time: 2150ms
                           Asset      Size  Chunks             Chunk Names
      ./0.6168aaac8461862eab7a.js  22.5 kB       0  [emitted]
   ./main.a2e49a279552980e3b91.js    60 kB       1  [emitted]  main
 ./vendor.ff9f7ea865884e6a84c8.js    46 kB       2  [emitted]  vendor
./runtime.25f5d0204e4f77fa57a1.js  1.45 kB       3  [emitted]  runtime

↓ Burada

[3IRH] ./index.js 29 kB {1} [built]
[DuR2] (webpack)/buildin/global.js 488 bytes {2} [built]
[JkW7] (webpack)/buildin/module.js 495 bytes {2} [built]
[LbCc] ./webPlayer.js 24 kB {1} [built]
[lebJ] ./comments.js 58 kB {0} [built]
[02Tr] ./ads.js 74 kB {1} [built]
    + 1 hidden module

Bu yaklaşımda, modülün kimliği yalnızca modülü yeniden adlandırdığınızda veya taşırsanız değişir. Yeni modüller diğer modüllerin kimliklerini etkilemeyecek.

Etkinleştirmek için eklentiyi yapılandırmanın plugins bölümüne ekleyin:

// webpack.config.js
module.exports = {
  plugins: [
    new webpack.HashedModuleIdsPlugin()
  ]
};

Daha fazla bilgi

Özet

  • Paketi önbelleğe alın ve paket adını değiştirerek sürümleri birbirinden ayırt edin
  • Paketi uygulama kodu, tedarikçi kodu ve çalışma zamanına ayırın
  • HTTP isteğini kaydetmek için çalışma zamanını satır içine alma
  • import ile kritik olmayan kodları geç yükleme
  • Gereksiz öğelerin yüklenmesini önlemek için kodu rotalara/sayfalara göre böl