几种清除多余CSS的方法和基本原理

最近遇到一个坑,重构给了重构代码,但是没有分离好,导致我引入了很多无用的css。这些css又引入了很多无用的图片,使整个的非常重而且很无用。我需要想办法把整个无用css都去掉。下面是几种方法。


gulp-uncss

gulp-uncss是一个gulp插件,和普通gulp插件没有区别。先引入gulp。然后利用uncss方法传入需要优化的css所在的页面。注意这里支持本地文件,正则匹配和url匹配。

1
2
3
4
5
6
7
8
9
10
var gulp = require('gulp');
var uncss = require('gulp-uncss');

gulp.task('default', function () {
return gulp.src('site.css')
.pipe(uncss({
html: ['index.html', 'posts/**/*.html', 'http://example.com']
}))
.pipe(gulp.dest('./out'));
});

当然,也可以和其他插件一起用。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var gulp = require('gulp');
var uncss = require('gulp-uncss');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var nano = require('gulp-cssnano');

gulp.task('default', function () {
return gulp.src('styles/**/*.scss')
.pipe(sass())
.pipe(concat('main.css'))
.pipe(uncss({
html: ['index.html', 'posts/**/*.html', 'http://example.com']
}))
.pipe(nano())
.pipe(gulp.dest('./out'));
});

我觉得很方便,最后项目里就是采用的这个方法。


chrome浏览器audits

利用chrome的自带审计功能。打开chrome浏览器,然后点击Audits审计功能。然后点击run,就可以分析出咱们这个页面可以优化的地方了。

图片指示

但是这有一个缺陷,就是它只能找到你的有多少样式是不需要的,不能够自动把清理后的css给你。你还是需要手动去你的文件里对比,然后删除。对于比较少的可以这么用。要是本来就有很多很多无用的,那么就很不好使了。这个时候,火狐出来了。


firefox浏览器css usage

下载css usage这个火狐插件并安装,地址在 https://addons.mozilla.org/en-US/firefox/addon/css-usage/。然后f12,切到CSS Usage 选项卡
点击 scan 按钮,稍后会分析出哪些css规则未使用。然后点击 export cleaned css 按钮,导出清理好的css文件,将在新页面打开新的css源文件。即可。是不是很方便。步骤是(Scan->Clear->AutoScan)

Scan: 通过字面意思我们就能知道,这是一个扫描当前页面的工具,如果我们的站点只有一个页面或者几个页面,我们可以通过使用此功能按键来查看页面的css实用情况.

Clear: 清除扫描结果,但我们查看完网页,并对CSS 进行了修改后,我们就不需要以前的扫描结果了,那么我们就可以使用Clear功能键,清除以前的扫描结果缓存,重新开始我们的扫描.

AutoScan: 我们的网站可能会有很多的页面,更有可能有很多的弹出层,如果我们每次都点击扫描的话,会占用我们大量的时间,AutoScan功能键可以使我们的扫描工作更自动化,提高我们的工作效率.


tidycss-nodejs插件

经常看到有童鞋问,有没有什么工具能快速分析出站点的CSS冗余,于是就有了这个项目。本质上,这个工具是为了解决我们 腾讯课堂 在多人开发与快速迭代下的CSS冗余问题,为代码Review提供可行的工具。-from tidycss github

install tidycss后,使用nodejs运行,之后并会生成报表。基本原理思想跟上面几个都差不多。源码也是利用selector。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var tidy = require('tidycss');

tidy(
// 你要检测冗余的url
'http://ke.qq.com',
// 可选参数
{
// 不对common.xxxx.css检测冗余,因为这个是站点公共文件
ignore: /common\..*\.css/,
// 忽略的选择器列表, 即这里的选择器是被review后可冗余项,
// 比如有通过javascript动态生成的DOM树
unchecks: ['.mod-nav__course-all span:hover']
}
);


可能的坑

这中间可能就是要尽量的把用到的css功能都拉出来。比如有一个模块,是我点击按钮才能出现列表。那么如果你不点击按钮,这些插件就获取不到列表这些css对应的dom,也就会认为这些css选择器是无用的。就会把这些选择器给删掉。那么真正的你是缺少这些css的。这个可能结合后面基本原理理解会更清楚。


基本原理

那么这些工具是如何做到识别没有使用过的css呢?

一个 css 选择器是无效的,也就是说我们是无法通过这个css选择器找到dom元素。所以,我们可以使用querySelector判断改css选择器对应的dom是否为空。从而知道哪些是没有使用的。

其实上面这个gulp的插件gulp-uncss是利用的另外一个别人写好的模块uncss,只不过把它打包成了gulp的插件格式。

我去看了下它的(uncss)源码,基本上就是上面那个思想,找出unused的selector,找出used过了的selector。然后看了下别人写的代码,就觉得还要好好努力,差的太多。下面放出核心代码,方便以后我经常学习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
'use strict';

var promise = require('bluebird'),
phantom = require('./phantom.js'),
postcss = require('postcss'),
_ = require('lodash');

/* Some styles are applied only with user interaction, and therefore its
* selectors cannot be used with querySelectorAll.
* http://www.w3.org/TR/2001/CR-css3-selectors-20011113/
*/
var dePseudify = (function () {
var ignoredPseudos = [
/* link */
':link', ':visited',
/* user action */
':hover', ':active', ':focus',
/* UI element states */
':enabled', ':disabled', ':checked', ':indeterminate',
/* pseudo elements */
'::first-line', '::first-letter', '::selection', '::before', '::after',
/* pseudo classes */
':target',
/* CSS2 pseudo elements */
':before', ':after',
/* Vendor-specific pseudo-elements:
* https://developer.mozilla.org/ja/docs/Glossary/Vendor_Prefix
*/
'::?-(?:moz|ms|webkit|o)-[a-z0-9-]+'
],
pseudosRegex = new RegExp(ignoredPseudos.join('|'), 'g');

return function (selector) {
return selector.replace(pseudosRegex, '');
};
}());

/**
* Private function used in filterUnusedRules.
* @param {Array} selectors CSS selectors created by the CSS parser
* @param {Array} ignore List of selectors to be ignored
* @param {Array} usedSelectors List of Selectors found in the PhantomJS pages
* @return {Array} The selectors matched in the DOMs
*/
function filterUnusedSelectors(selectors, ignore, usedSelectors) {
/* There are some selectors not supported for matching, like
* :before, :after
* They should be removed only if the parent is not found.
* Example: '.clearfix:before' should be removed only if there
* is no '.clearfix'
*/
return selectors.filter(function (selector) {
selector = dePseudify(selector);
/* TODO: process @-rules */
if (selector[0] === '@') {
return true;
}
for (var i = 0, len = ignore.length; i < len; ++i) {
if (_.isRegExp(ignore[i]) && ignore[i].test(selector)) {
return true;
}
if (ignore[i] === selector) {
return true;
}
}
return usedSelectors.indexOf(selector) !== -1;
});
}

/**
* Find which animations are used
* @param {Object} css The postcss.Root node
* @return {Array}
*/
function getUsedAnimations(css) {
var usedAnimations = [];
css.walkDecls(function (decl) {
if (_.endsWith(decl.prop, 'animation-name')) {
/* Multiple animations, separated by comma */
usedAnimations.push.apply(usedAnimations, postcss.list.comma(decl.value));
} else if (_.endsWith(decl.prop, 'animation')) {
/* Support multiple animations */
postcss.list.comma(decl.value).forEach(function (anim) {
/* If declared as animation, it should be in the form 'name Xs etc..' */
usedAnimations.push(postcss.list.space(anim)[0]);
});
}
});
return usedAnimations;
}

/**
* Filter @keyframes that are not used
* @param {Object} css The postcss.Root node
* @param {Array} animations
* @param {Array} unusedRules
* @return {Array}
*/
function filterKeyframes(css, animations, unusedRules) {
css.walkAtRules(/keyframes$/, function (atRule) {
if (animations.indexOf(atRule.params) === -1) {
unusedRules.push(atRule);
atRule.remove();
}
});
}

/**
* Filter rules with no selectors remaining
* @param {Object} css The postcss.Root node
* @return {Array}
*/
function filterEmptyAtRules(css) {
/* Filter media queries with no remaining rules */
css.walkAtRules(function (atRule) {
if (atRule.name === 'media' && atRule.nodes.length === 0) {
atRule.remove();
}
});
}

/**
* Find which selectors are used in {pages}
* @param {Array} pages List of PhantomJS pages
* @param {Object} css The postcss.Root node
* @return {promise}
*/
function getUsedSelectors(page, css) {
var usedSelectors = [];
css.walkRules(function (rule) {
usedSelectors = _.concat(usedSelectors, rule.selectors.map(dePseudify));
});
// TODO: Can this be written in a more straightforward fashion?
return promise.map(usedSelectors, function (selector) {
return selector;
}).then(function(selector) {
return phantom.findAll(page, selector);
});
}

/**
* Get all the selectors mentioned in {css}
* @param {Object} css The postcss.Root node
* @return {Array}
*/
function getAllSelectors(css) {
var selectors = [];
css.walkRules(function (rule) {
selectors = _.concat(selectors, rule.selector);
});
return selectors;
}

/**
* Remove css rules not used in the dom
* @param {Array} pages List of PhantomJS pages
* @param {Object} css The postcss.Root node
* @param {Array} ignore List of selectors to be ignored
* @param {Array} usedSelectors List of selectors that are found in {pages}
* @return {Object} A css_parse-compatible stylesheet
*/
function filterUnusedRules(pages, css, ignore, usedSelectors) {
var ignoreNextRule = false,
unusedRules = [],
unusedRuleSelectors,
usedRuleSelectors;
/* Rule format:
* { selectors: [ '...', '...' ],
* declarations: [ { property: '...', value: '...' } ]
* },.
* Two steps: filter the unused selectors for each rule,
* filter the rules with no selectors
*/
ignoreNextRule = false;
css.walk(function (rule) {
if (rule.type === 'comment') {
// ignore next rule while using comment `/* uncss:ignore */`
if (/^!?\s?uncss:ignore\s?$/.test(rule.text)) {
ignoreNextRule = true;
}
} else if (rule.type === 'rule') {
if (rule.parent.type === 'atrule' && _.endsWith(rule.parent.name, 'keyframes')) {
// Don't remove animation keyframes that have selector names of '30%' or 'to'
return;
}
if (ignoreNextRule) {
ignoreNextRule = false;
ignore = ignore.concat(rule.selectors);
}

usedRuleSelectors = filterUnusedSelectors(
rule.selectors,
ignore,
usedSelectors
);
unusedRuleSelectors = rule.selectors.filter(function (selector) {
return usedRuleSelectors.indexOf(selector) < 0;
});
if (unusedRuleSelectors && unusedRuleSelectors.length) {
unusedRules.push({
type: 'rule',
selectors: unusedRuleSelectors,
position: rule.source
});
}
if (usedRuleSelectors.length === 0) {
rule.remove();
} else {
rule.selectors = usedRuleSelectors;
}
}
});

/* Filter the @media rules with no rules */
filterEmptyAtRules(css);

/* Filter unused @keyframes */
filterKeyframes(css, getUsedAnimations(css), unusedRules);

return css;
}

/**
* Main exposed function
* @param {Array} pages List of PhantomJS pages
* @param {Object} css The postcss.Root node
* @param {Array} ignore List of selectors to be ignored
* @return {promise}
*/
module.exports = function uncss(pages, css, ignore) {
return promise.map(pages, function (page) {
return getUsedSelectors(page, css);
}).then(function (usedSelectors) {
usedSelectors = _.flatten(usedSelectors);
var filteredCss = filterUnusedRules(pages, css, ignore, usedSelectors);
var allSelectors = getAllSelectors(css);
return [filteredCss, {
/* Get the selectors for the report */
all: allSelectors,
unused: _.difference(allSelectors, usedSelectors),
used: usedSelectors
}];
});
};

总结

所以,如果是只是要一个整理后的文件,就用火狐就好。如果是工程化项目,用gulp的插件比较好。主要是整理了几种方法。中间也学到了不少东西,比如审计audits以前就没有关注过。现在知道是分析页面性能的一个好方法了。