背景
在小程序性能测试面板,发现有很多如下图所示的提示
当调用
setData 时,框架会执行以下步骤:
- 虚拟DOM树构建与对比:首先,框架会对新的数据进行分析,生成或更新对应的虚拟DOM树,并与现有页面的虚拟DOM树进行差异比较(diff算法)。
- DOM更新与重绘:基于比较结果,框架仅对发生改变的部分进行实际的DOM操作,包括插入、删除或更新节点,以及重新计算布局信息。
- 样式重排与重绘:对于受到影响的元素,还会触发CSS样式的重新计算(reflow)和元素内容及背景等的重新绘制(repaint)。
- 组件生命周期钩子:涉及变更的自定义组件会触发相应的生命周期方法,如
updated。
所以就算数据在页面没有使用,也会走第一步,导致不必要的CPU计算和内存消耗
编写脚本
实现思路
- 查找wxml里面使用的变量
- 对比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();
成果对比
优化之前:
优化之后:
总结
性能上并没有什么提升,主要提升在最佳实战,当然这是本地的性能体验数据,不知道线上会不会有什么改观,上线后观察一段时间再看