几种清除多余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以前就没有关注过。现在知道是分析页面性能的一个好方法了。