缩减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压缩)没法完成选择器名字的改变。这是由于CSS minifier没法控制HTML输出,同时,CSS文件名字会变长。webpack
若是你使用CSS模块,其文件名会包含样式表名字,本地标识符名字和随机的hash值。类名的模板为使用css-loader
的localIdentName参数来定义的。如[name]__[local]__[hash:base64:5]
。所以,生成的类名会像.MovieView__move-title__yvKVV
;若是喜欢描述符的话,还能够更长,如.MovieView___movie-description-with-summary-paragraph___yvKVV
git
然而,若是你使用webpack和babel-plugin-react-css-modules,你可使用css-loader
的getLocalIdent参数或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-modules
和css-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类名相同和偶然间生成的类名进入了广告拦截器的名单