项目实践 | 性能优化相关

137 阅读4分钟

滑动条防抖

防抖:滑动条频繁操作时只取最后一次操作的结果

<template>
     <el-slider size="small"
                :max="30"
                v-model="tempSecFilter"
                @input="changeSecFilter"
      />
</template>
<script>
  import _, { debounce } from "lodash";
  // 滑动条响应式和防抖
  let secFilter = ref(30)
  let tempSecFilter = ref(30)
  let changeSecFilter = debounce((value) => {
    secFilter.value = value;
    tempSecFilter.value = secFilter.value
   // secFilter更新会自动触发 DEScatter 更新
  }, 300) //防抖时间

  //手写防抖
  function debounce(fn, delay) {
    let timeout;
    return function (...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => { 
        fn.apply(this, args)
      }, delay);
    };
  }
</script>

时间切片优化渲染

原理: 把性能消耗大的操作切片,先执行第一片,让页面先渲染第一片,渲染完成后再操作下一片(在用户浏览的间隙在后台悄悄做)。绝对速度并未改变,但通过安排操作顺序,使首屏速度up,用户体验up。

怎么做? 把每个切片操作放到Requestanimation 回调,他执行完一个后会等浏览器渲染完成再执行下一个,自动完成分片执行操作。

前端: 把获取表格数据(文件)的操作分片,对每一片:

——> 先请求首屏要用的 50 条数据(url 里拼接起始行和结束行的参数)

——> 解析数据:如果是起始行号不是 0,则需要手动拼接一个字段行

——> 筛选数据、渲染(追加入响应式数据)

——> 在用户浏览的时候,在后台请求51-100的数据

——> ...

  // 优化:用时间分片请求数据:
  // 先请求首屏要用的 50 条数据 ——> 筛选、渲染 ——> 请求51-100的数据 ——> ...
  const loadDataByLine = async () => {
    let chunkSize = 50 //每片加载多少条数据
    let start = 0 
    let totalLines = 0 //总行数
    let header = null  //csv 的字段名
    function sliceRender() {
      requestAnimationFrame(async () => {
        const res = await axios.get(
          `http://localhost:3000/data/getTblByLine?start=${start}&end=${start+chunkSize}`
        );

        // 解析返回数据
        totalLines = res.data.totalLines
        let temp = []
        if (!header && start === 0) {
          // 如果是第一次加载,提取字段行
          header = res.data.lines[0];
        }
        if (start > 0 && header) {
          // 如果不是第一次加载,手动添加字段行
          temp.push(header);
        }
        temp.push(...res.data.lines);
        const csvString = temp.join('\n');
        dataAll = d3.csvParse(csvString, d3.autoType)

        // 初次筛选数据 ---> 初次渲染数据
        await processData();
        // console.log("本次请求:" + start + "-" + Number(start + chunkSize))

        // 如果还有未处理的数据,继续调度下一个切片
        start = start + chunkSize
        if (start < totalLines) {
          sliceRender();
        }
      });
    }
    sliceRender();
  }

3、后端

  • 使用 readline 模块逐行读取 csv 文件。
  • 根据请求参数,筛选特定范围的行,并以数组形式返回 lines
exports.getTblByLine = async function (req,res){
   
    const filePath = path.join( dataPath,'faced_result_clisa_cls2_12-24.csv');

    const startLine = parseInt(req.query.start) || 0;
    const endLine = parseInt(req.query.end) || startLine + 50;

    const readStream = fs.createReadStream(filePath);
    const rl = readline.createInterface({ input: readStream });

    let currentLine = 0;
    let lines = [];
    let totalLines = 0;
    // 筛选前端需要的行
    for await (const line of rl) {
        if (currentLine >= startLine && currentLine <= endLine) {
            lines.push(line);
        }
        // if (currentLine > endLine) break;
        currentLine++;
    }
    totalLines = currentLine; // 计算得到的总行数
  
    res.json({
        totalLines: totalLines,
        lines: lines
      });
    // res.json(lines);
}

表格懒加载

只显示可见区域的表格项:监控滚动事件 ---> 滚动时 ScrollTop 更新 ---> start 和 end 一起更新 ---> 截取 start-end 条显示 ---> 可见列表项始终只有 start-end 条

使用了自定义指令和节流技术实现了 el-table 的懒加载,具体思路如下:

  1. 注册自定义指令:绑定到 el-table 上,通过指令监听滚动事件。

  2. 获取滚动容器和表格主体:自定义指令中,获取: el-table 的

    • 滚动容器高度(.el-table__body-wrapper
    • 表格主体高度(可视)
    • 表格项高度
  3. 计算渲染范围:根据滚动容器的 scrollTop 和 固定表格项高度itemHeight 值,实时计算需要渲染的表格项的起点和终点。( computed里 ↓)

    • start = scrollTop / itemHeight
    • end = ( scrollTop + tableVisHeight ) / itemHeight
  4. 渲染数据:切割并渲染 start-end 的数据项

  5. 动态填充距离:根据计算出的起点和终点,动态设置表格主体的上下填充距离(padding 用来撑起容器的高度),保持表格容器总高度的一致性。

    • paddingTop = start * itemHeight
    • paddingBottom =(dataLen - end)* itemHeight
  6. 节流滚动事件处理:使用 lodash 库的 throttle 限制滚动事件的触发频率,确保在特定时间段内只执行一次滚动处理函数。

<template>
  <el-table 
    v-scroll
     :data="tableData.slice(start,end)"   <!--//4.切割割+渲染数据-->
     height = "266"
  >
    ....
  <el-table>
</el-table>

<script setup>
  //...
  // 定义滚动相关的响应式数据
  let tableData = ref([...]) //表格数据
  const scrollTop = ref(0); 
  const tableVisHeight = ref(226); //表格的可视区高度
  const itemHeight = ref(42); // 每个表格项高度

  // 3. 计算需要渲染范围
  const start = computed(() => {
    return Math.max(scrollTop.value / itemHeight.value - 5, 0);
  });
  const end = computed(() => {
    return Math.min((scrollTop.value + tableVisHeight.value) / itemHeight.value + 5, tableData.value.length);
  });
  
  // 5. 计算滚动条的上下填充距离
  const padding = computed(() => {
    let paddingBottom = (tableData.value.length - end.value) * itemHeight.value;
    let paddingTop = start.value * itemHeight.value;
    return [paddingTop, paddingBottom];
  });
   
  // 1. 自定义指令
  const tableRef = ref(null)
  const vScroll = {
    mounted: (el) => {
      // const tableWrapper = el.querySelector(".el-table__body-wrapper");//在此处无效,原因不明
      const tableWrapper = tableRef.value.$refs.scrollBarRef.wrapRef;
      // 6. 优化后的节流操作
      if (tableWrapper) {
        tableWrapper.addEventListener("scroll", throttle(() => handleScroll(tableWrapper), 200));
      } else {
        console.error("Table wrapper element not found");
      }
    }
  };

  // 滚动事件处理函数
  let handleScroll = (tableWrapper) => {
    const tableBody = tableWrapper.querySelector(".el-table__body");
    const tableItem = tableWrapper.querySelector(".el-table__row");
    if (tableBody && tableItem) {
       // 2. 获取滚动容器和表格主体(一次即可)
      itemHeight.value = tableItem.clientHeight;
      tableVisHeight.value = tableWrapper.clientHeight;

      // 3. 实时更新ScrollTop ---> start和end更新
      scrollTop.value = tableWrapper.scrollTop; 
      // 4. 更新上下填充距离
      tableBody.style.paddingTop = padding.value[0] + "px";
      tableBody.style.paddingBottom = padding.value[1] + "px";

    }
  }
  
</script>