一个脚本优化小程序的非必要setData

139 阅读3分钟

背景

在小程序性能测试面板,发现有很多如下图所示的提示

企业微信截图_17068638353892.png 当调用 setData 时,框架会执行以下步骤:

  1. 虚拟DOM树构建与对比:首先,框架会对新的数据进行分析,生成或更新对应的虚拟DOM树,并与现有页面的虚拟DOM树进行差异比较(diff算法)。
  2. DOM更新与重绘:基于比较结果,框架仅对发生改变的部分进行实际的DOM操作,包括插入、删除或更新节点,以及重新计算布局信息。
  3. 样式重排与重绘:对于受到影响的元素,还会触发CSS样式的重新计算(reflow)和元素内容及背景等的重新绘制(repaint)。
  4. 组件生命周期钩子:涉及变更的自定义组件会触发相应的生命周期方法,如updated

所以就算数据在页面没有使用,也会走第一步,导致不必要的CPU计算和内存消耗

编写脚本

实现思路

  1. 查找wxml里面使用的变量
  2. 对比js里setData的数据,如果不存在变量里面,说明没有使用,提出来通过this.data.xxx赋值

完整代码

const fs = require('fs');
const path = require('path');
const csvWriter = require('csv-writer').createObjectCsvWriter;
let allDeletedComponents = []

// 1. 遍历整个项目的 wxml 文件,提取变量名
function findVariablesInWXML(directory) {
    const files = fs.readdirSync(directory);

    for (const file of files) {
        const filePath = path.join(directory, file);
        const stat = fs.statSync(filePath);

        // 文件夹递归处理
        if (stat.isDirectory()) {
            if (filePath.includes('miniprogram_npm') || filePath.includes('node_modules')) {
                // 跳过 miniprogram_npm 文件夹
                continue;
            }
            findVariablesInWXML(filePath); // 递归处理子目录
        } else if (path.extname(file) === '.wxml') {
            const content = fs.readFileSync(filePath, 'utf-8');
            // 使用正则表达式匹配 {{}} 内的内容
            const regex = /{{(.*?)}}/g;
            let matches = content.match(regex)?.map(item => {
                return replaceSingleQuotedContent(item)
            })?.map(item => {
                return replaceSpecCharsWithSpace(item)
            })?.map(item => {
                return formatPriceString(item).split(' ')
            }).flat()
            const setmatches = [...new Set(matches)]
            readJsonFile(filePath, setmatches)
        }
    }
}
// 替换单引号及单引号内部内容 如:item.id === activityId ? 'linktrack-home-findprop' : '' => item.id === activityId ?  :
const replaceSingleQuotedContent = (inputString) => {
    return inputString.replace(/'(.*?)'/g, '');
};
// 替换特殊字符 如:item.id === activityId ?  :  => item.id     activityId
const replaceSpecCharsWithSpace = (inputString) => {
    return inputString.replace(/[?!+-=:*&||{}()/]/g, ' ');
};
// 多个空格替换为单个空格, 如:item.id     activityId => item.id activityId
const formatPriceString = (inputString) => {
    const str = inputString.trim().replace(/\s+/g, ' ');
    return str.replace(/./g, ' ');
};
// 冒号分割字符串
function splitString(inputString) {
    const index = inputString.indexOf(':');

    if (index !== -1) {
        const firstPart = inputString.substring(0, index);
        let secondPart = inputString.substring(index + 1).trim().replace(/,$/, '');
        const secontStr = secondPart.split('//')
        if (secontStr.length > 1) {
            secondPart = secontStr[0].trim().replace(/,$/, '') + ' // ' + secontStr[1]
        }
        return [firstPart.trim(), secondPart.trim()];
    }
    return [];
}
function readJsonFile(filePath, variables) {
    const regex = /^(.*?)(?=.wxml)/; // 匹配wxml对应的js文件
    const match = filePath.match(regex);

// 获取匹配到的内容
    const contentBeforeWXML = match ? match[1] : '';
    let content;

    const fileExtensions = ['js', 'ts'];
    let fileWithPath = ''
    for (const extension of fileExtensions) {
        fileWithPath = `${contentBeforeWXML}.${extension}`;

        try {
            content = fs.readFileSync(fileWithPath, 'utf-8');
            break; // 如果成功读取文件,则跳出循环
        } catch (error) {
            if (error.code !== 'ENOENT') {
            }
        }
    }
    content && handleJsFile(content, variables, fileWithPath)

}
function handleJsFile(code, variables, fileWithPath) {
    let fileArr = []
    const lines = code?.split('\n');
    let startIng = false;
    let removeKey = new Map();
    let stack = []; // 括号配对
    const modifiedLines = lines.map(line => {
        // 匹配到this.setData({,则进入setData代码块
        startIng = startIng ? startIng : (line.includes('this.setData({') || line.includes('that.setData({'))
        if (!startIng) {
            // 非setData代码块,直接返回
            removeKey.clear()
            return line
        }
        if (startIng) {
            // 匹配到setData结束
            if ( line.includes('})')) {
                startIng = false
                // 如果有需要删除的属性,则添加到代码块末尾
                if (removeKey.size > 0) {
                    let _line = line
                    removeKey.forEach((value, key) => {
                        _line = `${_line}
                        this.data.${key} = ${value}`
                    })
                    return _line
                } else {
                    return line
                }
            }
            // {开始 一下两个if是为了匹配{}的配对,防止误删,如:setData({attr: {a: 1}}),不应该删除a:1,只删除setData的一级属性
            if (line.trim().endsWith('{') && !line.includes('setData({')) {
                stack.push('{')
                return line
            }
            // }结束
            if (line.trim().startsWith('}') && !line.includes('})')) {
                if (stack[stack.length - 1] === '{') {
                    stack.pop()
                    if (stack.length) {
                        return line
                    }
                } else {
                    stack.push('}')
                }
            }
            const [key, value] = splitString(line.trim()) // line.split(':').map(str => str.trim());
            if (key && value ) { // 设置属性
                const attrRegex = /^[a-zA-Z][a-zA-Z_-]*[a-zA-Z]$/; // 匹配合法的属性名
                if (!stack.length && !variables.includes(key) && attrRegex.test(key)) { // 如果是合法的属性名,且不在wxml中使用,则删除
                    removeKey.set(key, value.replace(/,\s*$/, ''))
                    fileArr.push(key)
                    return '';
                }
                return line;

            } else {
                return line
            }
        }
    });
    // 如果有需要删除的属性,则记录下来,用于生成CSV文件
    if (fileArr.length) {
        allDeletedComponents.push({
            jsFile: fileWithPath,
            variable: fileArr.join(',')
        });
    }
    const modifiedCode = modifiedLines.join('\n');
    fs.writeFileSync(fileWithPath, modifiedCode, 'utf-8');
}

// 生成 CSV 文件
function generateCsvFile(deletedComponents) {
    const csvFilePath = path.join(__dirname, 'deleted_variable.csv');
    const csvWriterInstance = csvWriter({
        path: csvFilePath,
        header: [
            { id: 'jsFile', title: '变量所在位置' },
            { id: 'variable', title: '涉及的变量' }
        ]
    });
    csvWriterInstance.writeRecords(deletedComponents)
        .then(() => console.log(`已生成 CSV 文件:${csvFilePath}`))
        .catch(error => console.error('生成 CSV 文件时发生错误:', error));
}
// 入口函数
function main() {
    const projectDirectory = '....'; // 小程序项目的根目录
    findVariablesInWXML(projectDirectory);

    if (allDeletedComponents.length > 0) {
        generateCsvFile(allDeletedComponents);
    } else {
        console.log('没有删除任何组件。');
    }
}

main();

成果对比

优化之前:

企业微信截图_17070155475571.png 优化之后:

企业微信截图_17070155592086.png

总结

性能上并没有什么提升,主要提升在最佳实战,当然这是本地的性能体验数据,不知道线上会不会有什么改观,上线后观察一段时间再看