[譯]Reducing CSS bundle size 70% by cutting the class names and using scope isola

縮減CSS bundle大小一直是個很大的問題,因此有了CSS Module,而本文做者經過觀察谷歌首頁CSS類名命名方法,使用縮小CSS類名而且還經過做用域隔離最終減小本身項目中css bundle。css

做者創建了一個訂電影票項目GO2CINEMA,做者爲了讓其更加快捷方便和安全,採用了ūsus預渲染HTML文件,ūsus能渲染SPA的HTML,而且能引入CSS,用於渲染網頁,可是其不想引入70kb文件給每一個HTML文件,並且其中大部分都是CSS類名致使的,因此纔有了本文經過縮小css類名和使用做用域隔離方法來縮減CSS包大小,從最初的140kb到最後的47kb。node

學谷歌那樣作

若是你曾看過谷歌首頁的源碼,你會注意到CSS的類名不會超過兩個字符長度react

CSS minifier的缺點

CSS minifier(CSS壓縮)沒法完成選擇器名字的改變。這是由於CSS minifier沒法控制HTML輸出,同時,CSS文件名字會變長。webpack

若是你使用CSS模塊,其文件名會包含樣式表名字,本地標識符名字和隨機的hash值。類名的模板爲使用css-loaderlocalIdentName參數來定義的。如[name]__[local]__[hash:base64:5]。所以,生成的類名會像.MovieView__move-title__yvKVV;若是喜歡描述符的話,還能夠更長,如.MovieView___movie-description-with-summary-paragraph___yvKVVgit

在編譯時期更改CSS類名

然而,若是你使用webpack和babel-plugin-react-css-modules,你可使用css-loadergetLocalIdent參數或babel-plugin-react-css-modules的generateScopedName在編譯時期更更名字。github

const generateScopedName = (
  localName: string,
  resourcePath: string
) => {
  const componentName = resourcePath.split('/').slice(-2, -1);
return componentName + '_' + localName;
};複製代碼

generateScopeName還有一個優勢:一樣的例子能夠用於Babel和webpack。web

/** * @file Webpack configuration. */
const path = require('path');

const generateScopedName = (localName, resourcePath) => {
  const componentName = resourcePath.split('/').slice(-2, -1);

  return componentName + '_' + localName;
};

module.exports = {
  module: {
    rules: [
      {
        include: path.resolve(__dirname, '../app'),
        loader: 'babel-loader',
        options: {
          babelrc: false,
          extends: path.resolve(__dirname, '../app/webpack.production.babelrc'),
          plugins: [
            [
              'react-css-modules',
              {
                context: common.context,
                filetypes: {
                  '.scss': {
                    syntax: 'postcss-scss'
                  }
                },
                generateScopedName,
                webpackHotModuleReloading: false
              }
            ]
          ]
        },
        test: /\.js$/
      },
      {
        test: /\.scss$/,
        use: [
          {
            loader: 'css-loader',
            options: {
              camelCase: true,
              getLocalIdent: (context, localIdentName, localName) => {
                return generateScopedName(localName, context.resourcePath);
              },
              importLoaders: 1,
              minimize: true,
              modules: true
            }
          },
          'resolve-url-loader'
        ]
      }
    ]
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: path.join(__dirname, './.dist'),
    publicPath: '/static/'
  },
  stats: 'minimal'
};複製代碼

使名字短點

因爲babel-plugin-react-css-modulescss-loader採用一樣邏輯產生CSS類名,咱們能夠將類名改爲任何所想的,甚至是隨機的hash值。然而,咱們更想要的儘量短的類名。算法

爲了產生最短的類名,建立類名索引和使用incstr產生遞增的id用於索引值。安全

評論:incstr會按照你所給的模板字符串來自動生成類名,如模板爲0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,每次運行時會生成下一個字符串babel

let i = incstr() //"0",模板中第一位爲0
i = incstr(i) //"1",模板中第二位爲1
//...
i = incstr(i) //"Z",已經到模板中最後一位,而後就開始生成兩位的
i = incstr(i) //"00",仍是先從0開始
i = incstr(i) //"01"複製代碼
const incstr = require('incstr');

const createUniqueIdGenerator = () => {
  const index = {};

  const generateNextId = incstr.idGenerator({
    // 模板中沒有d,是爲了不出現ad的狀況
    // 由於類名或其前綴爲ad,會形成被攔截廣告插件攔截
    //詳細狀況看https://medium.com/@mbrevda/just-make-sure-ad-isnt-being-used-as-a-class-name-prefix-or-you-might-suffer-the-wrath-of-the-558d65502793
    alphabet: 'abcefghijklmnopqrstuvwxyz0123456789'
  });

  return (name) => {
    if (index[name]) {
      return index[name];
    }

    let nextId;

    do {
      // 類名不能是數字開頭
      nextId = generateNextId();
    } while (/^[0-9]/.test(nextId));

    index[name] = generateNextId();

    return index[name];
  };
};

const uniqueIdGenerator = createUniqueIdGenerator();

const generateScopedName = (localName, resourcePath) => {
  const componentName = resourcePath.split('/').slice(-2, -1);

  return uniqueIdGenerator(componentName) + '_' + uniqueIdGenerator(localName);
};複製代碼

這樣咱們的類名就會變成.a_a, .b_a等等。

到這裏,能讓項目的css包從140kb減小到53kb

使用做用域隔離方法減小包大小

在類名中添加_來隔離組件名和本地標識符名,這種作法對於減少bundle大小是頗有用的。

csso(CSS minifier)中的scopes參數,Scope定義了相同標識符的類名列表,如不一樣做用域的選擇器沒法匹配相同元素,這參數容許優化器更激進地移除一些規則。

評論:csso中舉個例子以下:
假設有個文件

.module1-foo { color: red; }
.module1-bar { font-size: 1.5em; background: yellow; }
.module2-baz { color: red; }
.module2-qux { font-size: 1.5em; background: yellow; width: 50px; }複製代碼

而後在scopes中填入下面的類名列表

{
    "scopes": [
        ["module1-foo", "module1-bar"],
        ["module2-baz", "module2-qux"]
    ]
}複製代碼

最終生成:
module1-foo,.module2-baz{color:red}.module1-bar,.module2-qux{font-size:1.5em;background:#ff0}.module2-qux{width:50px}

利用這,使用csso-webpakck-plugin來預處理CSS包。

const getScopes = (ast) => {
  const scopes = {};

  const getModuleID = (className) => {
    const tokens = className.split('_')[0];

    if (tokens.length !== 2) {
      return 'default';
    }

    return tokens[0];
  };

  csso.syntax.walk(ast, node => {
    if (node.type === 'ClassSelector') {
      const moduleId = getModuleID(node.name);

      if (moduleId) {
        if (!scopes[moduleId]) {
          scopes[moduleId] = [];
        }

        if (!scopes[moduleId].includes(node.name)) {
          scopes[moduleId].push(node.name);
        }
      }
    }
  });

  return Object.values(scopes);
};複製代碼

這樣會讓項目CSS bundle從53kb變成47kb

這樣值得嗎

第一點可使用壓縮算法來減少。可是使用Brotli僅僅減小了1kb

另外一方面,創建此減少措施是一次性的,它會在每次編譯後都會減少。還有其餘益處,如會避免同以前CSS類名相同和偶然間生成的類名進入了廣告攔截器的名單

原文連接

原文


歡迎訂閱掘金專欄知乎專欄

相關文章
相關標籤/搜索