自己写的一个css性能分析工具:tinycss

Posted caoke

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自己写的一个css性能分析工具:tinycss相关的知识,希望对你有一定的参考价值。

## tinycss 
分析一个css文件在多个html页面的使用情况,输出有效的css文件

### 技术栈
glob、postcss、vue-template-compiler

### 开发准备

1、git clone本项目~

2、cnpm i

### 使用说明:
1、src目录放入html文件(一个以上)、css文件(一个以上)
2、npm run test //执行命令,输出dist目录
3、demo.css是压缩后的css文件,demo.map是矩阵数据(记录css的命中情况)

### 注意点
不支持所有伪类(除了:root),例如:div.class:first-child 等同于 div.class

### 不足:
不过滤动画选择器:keyframes
不支持去重,不支持选择器、属性去重



核心类CssRect.js
const Api=require(‘./Api‘);
//解析成语法树
const compiler = require(‘vue-template-compiler‘);
const postcss  = require(‘postcss‘);

/*css rule矩阵,3*6
行对应selector[‘.id‘,‘.class1‘,‘.class2‘]
列对应html节点 [‘body‘,‘body div‘,‘body div div‘,‘body div p‘,‘body div span‘,‘body div span a‘]
[
    [0,0,0,0,1,0],
    [0,0,0,0,1,0],
    [0,0,0,0,1,0]
]
*/
class CssRect

    constructor(htmlText,cssText,debug)

        //log
        this.logCache=
        this.debug=debug;

        //记录selector查找历史
        this.selectotCache=;

        this.cssText=cssText;
        //构建html语法树和矩阵bitmap
        this.htmlAst=compiler.compile(htmlText).ast;
        this.htmlList=Api.depthSearch(this.htmlAst).filter(function (node) 
            return node.type===1;
        )

        //构建css语法树和矩阵bitmap
        const cssObj=CssRect.getCssAstAndList(cssText);
        this.cssAst=cssObj.cssAst;
        this.cssList=cssObj.cssList
    
    static getCssAstAndList(cssText)
        const obj=
        obj.cssAst=postcss.parse(cssText);
        obj.cssList=Api.depthSearch(obj.cssAst,‘nodes‘).filter(function (node) 
            return node.type===‘rule‘&&!/keyframes/.test(node.parent.name);
        )
        return obj;
    
    //分析
    analysis()
        const cssList=this.cssList;
        const map=[]
        for(let i=0;i<cssList.length;i++)
            map[i]=this.querySelector(cssList[i].selector);
        
        return map;
    
    //可能是多选择器
    querySelector(selector)
        if(/,/.test(selector))
            const arr=selector.split(‘,‘);
            const data=this.queryOneSelector(arr[0]);
            for(let i=1;i<arr.length;i++)
                const item=this.queryOneSelector(arr[i]);
                for(let k=0;k<data.length;k++)
                    if(data[k]==0)
                        data[k]=item[k];
                    
                
                return data;
            
        else
            return this.queryOneSelector(selector)
        
    
    //查询css_rule,返回[array astNode]
    queryOneSelector(selector)
        selector=selector.trim();//去掉左右空格

        //解析css rule
        const selectorArr=[]
        selector.replace(/(.+?)([ >~\+]+(?!\d)|$)/ig,function (m,p1,p2) 
            selectorArr.push(p1,p2);
        )
        // console.log(selectorArr)
        this.selectorArr=selectorArr;
        // console.log(selectorArr)
        //设置缓存
        let preSelector=‘‘;
        for(let i=0;i<selectorArr.length;i=i+2)
            const exec=selectorArr[i-1]||‘‘;
            const curSelector=selectorArr[i];

            this.setSelectotCache(preSelector,exec,curSelector);
            preSelector=preSelector+exec+curSelector
        
        const arr=new Array(this.htmlList.length).fill(0);
        // if(‘:root body‘==selector)
        // console.log(selector,selectorArr)
        this.selectotCache[selector].forEach( (node) =>
            arr[this.htmlList.indexOf(node)]=1;
        )
        return arr;
    
    //记录selector查询html语法树
    setSelectotCache(preSelector,exec,curSelector)
        const nextSelector=preSelector+exec+curSelector;
        //已有缓存
        if(this.selectotCache[nextSelector])return;
        if(!preSelector&&!exec)
            this.selectotCache[curSelector]=this.breadthHit(curSelector,this.htmlAst)
            return;
        
        const arr=this.selectotCache[preSelector];

        this.selectotCache[nextSelector]=[];
        if(/^ +$/.test(exec))
            arr.forEach((node)=>
                this.selectotCache[nextSelector]=this.selectotCache[nextSelector].concat(this.breadthHit(curSelector,node));
            )
        else if(/^ *> *$/.test(exec))
            arr.forEach((node)=>
                this.selectotCache[nextSelector]+this.selectotCache[nextSelector].concat(this.childHit(curSelector,node));
            )
        else if(/^ *\+ *$/.test(exec))
            arr.forEach((node)=>
                this.selectotCache[nextSelector]+this.selectotCache[nextSelector].concat(this.sublingHit(curSelector,node));
            )
        else if(/^ *~ *$/.test(exec))
            arr.forEach((node)=>
                this.selectotCache[nextSelector]+this.selectotCache[nextSelector].concat(this.sublingsHit(curSelector,node));
            )
        else
            console.log(‘exec异常:‘+exec)
        

    
    //css_rule:element+element
    sublingHit(tag,astNode)
        if(!astNode.parent)
            return [astNode].filter( (node) =>
                return this.hitNode(tag,node);
            )
        
        return Api.nextSublingSearch(astNode,astNode.parent).filter( (node) =>
            return this.hitNode(tag,node);
        )
    
    //css_rule:element~element
    sublingsHit(tag,astNode)
        return Api.nextSublingsSearch(astNode,astNode.parent).filter(function (node) 
            return this.hitNode(tag,node);
        )
    
    //css_rule:element element
    breadthHit(tag,astNode)
        return Api.breadthSearch(astNode).filter( (node)=> 
            return node.type===1&&this.hitNode(tag,node);
        )
    
    //css_rule:element>element
    childHit(tag,astNode)
        return Api.childSearch(astNode).filter( (node)=> 
            return node.type===1&&this.hitNode(tag,node);
        )
    
    //log 一次
    logOnce(key)
        if(!this.debug)return;
        if(this.logCache[key])
            return;
        
        this.logCache[key]=true;
        console.log.apply(console,arguments)
    
    //tag是否命中ast节点,返回true、false
    hitNode(selector,astNode) 
        //分割字符串 (tag)、(id、class)(val)
        if(selector===‘*‘)
            return true;
        else if(/:root/.test(selector))
            return astNode.tag===‘html‘;
        else
            const arr=[];
            //tag
            if(/(^[a-z]+)/i.test(selector))
                const tag=RegExp.$1;
                arr.push(astNode.tag===tag)
            
            //class
            if(/\.([\w-]+)/.test(selector))
                const val=RegExp.$1;
                arr.push(astNode.attrsMap.class&&astNode.attrsMap.class.split(‘ ‘).indexOf(val)>-1);
            
            //id
            if(/#(\w+)/.test(selector))
                const val=RegExp.$1;
                arr.push(astNode.attrsMap.id===val);
            
            //属性
            if(/\[([\w-]+)(~=|=||=)?(\w+)?\]/.test(selector))
                const key=RegExp.$1;
                const exec=RegExp.$2;
                const val=RegExp.$3;
                this.logOnce(selector,‘属性选择器,只判断是否存在属性‘)
                arr.push(astNode.attrsMap[key]===true);
            
            //伪类选择器
            if(/(\:.+)/.test(selector))
                const key=RegExp.$1;
                this.logOnce(selector,‘伪类选择器,不解析‘)
                arr.push(true)
                // arr.push(astNode.attrsMap.id===val);
            
            if(arr.length==0)
                // console.log(this.selectorArr)
                console.log(selector,this.selectorArr,‘css 解析异常‘)
            
            return arr.every((item)=>item);
        



    

module.exports=CssRect;

 

应用类TinyCss.js

const CssRect=require(‘./CssRect‘)

//构建出一个css语法树和多个html语法书,分析css的使用率。
class TinyCss
    constructor(htmlTextArr,cssText)

        //多个html书法树
        this.htmlTextArr=htmlTextArr;

        //一个css书法树
        this.cssText=cssText;

    
    //移除数组中的子元素
    removeObj(item,arr)
        for(let i=0;i<arr.length;i++)
            if(arr[i]===item)
                arr.splice(i,1)
                break;
            
        
    
    //获取矩阵数据
    getBigMap()
        let map=[];
        for(let i=0;i<this.htmlTextArr.length;i++)
            const htmlText=this.htmlTextArr[i];
            const ccRect=new CssRect(htmlText,this.cssText);
            const rect=ccRect.analysis();
            map.push(rect)
        
        return map;
    
    //获取小数据,矩阵数据
    getMap()
        let map=[];
        for(let i=0;i<this.htmlTextArr.length;i++)
            const htmlText=this.htmlTextArr[i];
            const ccRect=new CssRect(htmlText,this.cssText);
            const rect=ccRect.analysis();

            const arr=rect.map(function (item) 
                return item.reduce((x,y)=>x+y);
            );
            for(let j=0;j<arr.length;j++)
                if(!map[j])map[j]=[];
                map[j].push(arr[j])
            
        
        return map;
    
    //获取展示数据
    showMap()
        const cssObj=CssRect.getCssAstAndList(this.cssText);

        const map=this.getMap();
        for(let i=0;i<map.length;i++)
            map[i]=cssObj.cssList[i].selector+","+map[i].join(‘,‘);
        
        return map;
    
    //显示无用的css
    getEmptyCss()
        const cssObj=CssRect.getCssAstAndList(this.cssText);

        const data=[];
        const map=this.getMap();
        for(let i=0;i<map.length;i++)
            //存在比0大的就是用到的,都是0就是无用的css
            if(map[i].every(function (n) 
                return n===0
            ))
                //从ast中移除节点
                this.removeObj(cssObj.cssList[i],cssObj.cssList[i].parent.nodes);
                data.push(cssObj.cssList[i].selector);
            
        
        this.tinyAst=cssObj.cssAst;
        return data;
    
    getTinyAst()
        if(!this.tinyAst)
            this.getEmptyCss();
        
        return this.tinyAst;
    

module.exports=TinyCss;

  

运行app.js

const TinyCss=require(‘./TinyCss‘)

const fs=require(‘fs‘);
const path=require(‘path‘);
const glob=require(‘glob‘);

//多个html文件
const htmlFileArr=glob.sync(‘./src/*.html‘);
const htmlTextArr=htmlFileArr.map(function (filepath) 
    return fs.readFileSync(filepath).toString()
)
// //多个css文件
const cssFileArr=glob.sync(‘./src/*.css‘);
// console.log(cssFileArr)
const cssText=cssFileArr.map(function (filepath) 
    return fs.readFileSync(filepath).toString()
).join(‘‘);

//启动
const app=new TinyCss(htmlTextArr,cssText);
// console.log(htmlFileArr)
// console.log(app.showMap())
// console.log(app.getEmptyCss())


//输出
const toText=
    emptyCss:app.getEmptyCss(),
    showMap:app.showMap(),

if(!fs.existsSync(‘./dist‘))
    fs.mkdirSync(‘./dist‘);

if(!fs.existsSync(‘./src‘))
    fs.mkdirSync(‘./src‘);

fs.writeFileSync(‘dist/‘+path.basename(cssFileArr[0]),app.getTinyAst().toString());
fs.writeFileSync(`dist/$path.basename(cssFileArr[0],‘css‘)map`,JSON.stringify(toText,null,2))

 

工具Api.js,节点查询相关,深度遍历和广度遍历

const treeSearch=require(‘./treeSearch‘);
//遍历子节点
function childSearch(node,childProp=‘children‘)
    return node[childProp];

//遍历兄弟节点
function nextSublingsSearch(node,pnode,childProp=‘children‘)
    const parr=pnode[childProp].filter((node)=>
        return node.type===1
    );
    return parr.slice(parr.indexOf(node)+1);

//遍历下一个兄弟节点
function nextSublingSearch(node,pnode,childProp=‘children‘)
    return nextSublingsSearch(node,pnode).slice(0,1);

module.exports=
    childSearch,
    nextSublingsSearch,
    nextSublingSearch,
    ...treeSearch

 

数据输出:demo.map


  "htmlFileArr": [
"./src/xsj.html",
"./src/years_carnival.html",
"./src/years_carnival1.html",
"./src/zhima_recognition.html"
],
"emptyCss": [
".modal-center .wrap",
".modal-center .flex1",
".modal-center .flex1:first-child",
".modal-center .flex1:last-child,\n.modal-center .flex1.non-border",
".modal .align-left,\n.home-page .t-content .des,\n.over.face .des-failed",
".modal.recognition .close",
".modal.recognition .wrap .flex1"
],
"showMap": [
".btn,\n.btn-left,\n.btn-right,0,2,2,2",
".btn.active,0,2,2,2",
"body,1,1,1,1",
".agree-pact span,0,0,0,5",
".agree-pact,\n.form-group .form-control .dt,0,0,0,1",
".modal-center .wrap,0,0,0,0",
".modal-center .flex1,0,0,0,0",
".modal-center .flex1:first-child,0,0,0,0",
".modal-center .flex1:last-child,\n.modal-center .flex1.non-border,0,0,0,0",
".btn,\n.btn-left,\n.btn-right,0,2,2,2",
".btn.active,0,2,2,2",
".btn.disabled-btn,0,2,2,2",
".modal.entrance,0,1,1,0",
".modal .align-left,\n.home-page .t-content .des,\n.over.face .des-failed,0,0,0,0",
".modal.recognition .close,0,0,0,0",
".modal.recognition .wrap .flex1,0,0,0,0",
".modal.vcode .btn,0,1,1,0",
".modal.vcode .btn.disabled-btn,0,1,1,0",
".modal.share .modal-content,0,1,1,0",
".modal.share .btn,0,1,1,0",
".btn,\n.btn-left,\n.btn-right,0,2,2,2",
".btn.active,0,2,2,2"
]
 

 

以上是关于自己写的一个css性能分析工具:tinycss的主要内容,如果未能解决你的问题,请参考以下文章

《性能测试二三谈》系列

转网站前端性能优化之javascript和css

wrk 性能测试工具安装与使用

性能测试工具开发过程中遇到的问题汇总

有哪些前端框架是使用typescript写的?

CSS 性能分析器?