如果你正在开发需要展示大量数据的 Vue 应用,肯定遇到过这样的困扰:列表或表格数据量稍大,页面就开始卡顿、滚动迟滞,用户体验直线下降。传统的前端渲染方式在面对成千上万条数据时,往往力不从心。

今天要介绍的 vue-tui 虚拟滚动方案,正是为了解决这个痛点而生。但它的价值不止于"能滚动"——真正关键的是如何实现"超丝滑"的体验。很多开发者以为虚拟滚动只是简单的 DOM 复用,实际上,优秀的虚拟滚动方案需要在渲染性能、内存管理、滚动体验三个维度做到极致平衡。

本文将带你深入理解 vue-tui 虚拟滚动的实现原理,并通过完整示例演示如何在实际项目中实现丝滑的大数据列表渲染。无论你是正在优化现有项目的性能,还是为新产品技术选型,都能从中获得实用的解决方案。

1. 虚拟滚动解决的核心问题

1.1 传统渲染的性能瓶颈

在讨论解决方案前,先要清楚问题所在。传统的前端列表渲染存在两个致命缺陷:

内存占用爆炸 :假设有 1 万条数据,每条数据对应一个 DOM 节点,即使使用 CSS 优化,内存占用也会线性增长。在移动端或低配置设备上,这种内存压力会导致页面崩溃或频繁垃圾回收。

渲染性能瓶颈 :浏览器同时渲染大量 DOM 节点时,重排和重绘的成本呈指数级增长。即使数据已经加载完成,用户滚动时仍会感到明显的卡顿。

1.2 虚拟滚动的本质优势

虚拟滚动的核心思想很简单:只渲染可视区域内的内容。但这简单思想背后,隐藏着复杂的技术实现:

  • 视觉欺骗 :通过占位元素维持滚动条的正确比例
  • 动态渲染 :根据滚动位置实时计算需要渲染的条目
  • 内存回收 :及时销毁不可见的 DOM 节点,释放内存

vue-tui 在这方面做得更加彻底,它不仅实现了基础的虚拟滚动,还通过多项优化技术实现了"丝滑"的体验目标。

2. vue-tui 虚拟滚动的核心原理

2.1 基础架构设计

vue-tui 的虚拟滚动组件基于以下关键设计:

// 虚拟滚动的核心状态管理
class VirtualScrollCore {
  constructor() {
    this.visibleStartIndex = 0;    // 可见区域起始索引
    this.visibleEndIndex = 0;      // 可见区域结束索引
    this.overScanCount = 5;        // 前后预渲染条数
    this.itemHeight = 50;          // 单项高度(或动态高度计算)
    this.containerHeight = 0;      // 容器高度
    this.totalHeight = 0;          // 总高度(用于滚动条)
  }
  
  // 根据滚动位置计算可见区域
  calculateVisibleRange(scrollTop) {
    this.visibleStartIndex = Math.floor(scrollTop / this.itemHeight);
    this.visibleEndIndex = this.visibleStartIndex + 
      Math.ceil(this.containerHeight / this.itemHeight);
    
    // 添加预渲染缓冲
    this.visibleStartIndex = Math.max(0, this.visibleStartIndex - this.overScanCount);
    this.visibleEndIndex = Math.min(
      this.totalCount - 1, 
      this.visibleEndIndex + this.overScanCount
    );
  }
}

2.2 滚动性能优化技术

vue-tui 通过多种技术组合实现丝滑体验:

DOM 回收与复用 :创建固定数量的 DOM 节点池,通过数据绑定更新内容,避免频繁的 DOM 创建销毁。

滚动节流与防抖 :智能合并滚动事件,在保证响应性的同时减少计算次数。

异步渲染策略 :将渲染任务分解为多个微任务,避免阻塞主线程。

3. 环境准备与项目集成

3.1 安装与配置

首先确保你的项目环境符合要求:

# 检查 Vue 版本
vue --version
# 应该为 Vue 2.6+ 或 Vue 3.0+

# 安装 vue-tui
npm install vue-tui --save
# 或使用 yarn
yarn add vue-tui

3.2 基础项目结构

建议的项目结构如下:

src/
├── components/
│   ├── VirtualList.vue          # 虚拟列表组件
│   └── BusinessList.vue         # 业务列表组件
├── utils/
│   └── virtualScroll.js         # 虚拟滚动工具函数
└── views/
    └── DataListView.vue         # 数据列表页面

4. 基础虚拟列表实现

4.1 最小可行示例

让我们从最简单的虚拟列表开始:

<template>
  <div 
    class="virtual-list-container"
    @scroll="handleScroll"
    ref="container"
  >
    <div class="virtual-list-phantom" :style="phantomStyle"></div>
    <div class="virtual-list-content" :style="contentStyle">
      <div 
        v-for="item in visibleData"
        :key="item.id"
        class="virtual-list-item"
        :style="getItemStyle(item)"
      >
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualList',
  props: {
    listData: {
      type: Array,
      required: true
    },
    itemHeight: {
      type: Number,
      default: 50
    },
    bufferSize: {
      type: Number,
      default: 5
    }
  },
  data() {
    return {
      visibleStartIndex: 0,
      visibleEndIndex: 0,
      containerHeight: 0
    };
  },
  computed: {
    // 计算占位元素高度
    phantomStyle() {
      return {
        height: this.listData.length * this.itemHeight + 'px'
      };
    },
    // 计算内容区域定位
    contentStyle() {
      return {
        transform: `translateY(${this.visibleStartIndex * this.itemHeight}px)`
      };
    },
    // 获取可见数据
    visibleData() {
      return this.listData.slice(
        Math.max(0, this.visibleStartIndex - this.bufferSize),
        Math.min(this.listData.length, this.visibleEndIndex + this.bufferSize)
      );
    }
  },
  mounted() {
    this.initContainer();
    this.calculateVisibleRange();
  },
  methods: {
    initContainer() {
      this.containerHeight = this.$refs.container.clientHeight;
    },
    handleScroll(event) {
      const scrollTop = event.target.scrollTop;
      this.calculateVisibleRange(scrollTop);
    },
    calculateVisibleRange(scrollTop) {
      this.visibleStartIndex = Math.floor(scrollTop / this.itemHeight);
      this.visibleEndIndex = this.visibleStartIndex + 
        Math.ceil(this.containerHeight / this.itemHeight);
    },
    getItemStyle(item) {
      return {
        height: this.itemHeight + 'px',
        lineHeight: this.itemHeight + 'px'
      };
    }
  }
};
</script>

<style scoped>
.virtual-list-container {
  height: 400px;
  overflow-y: auto;
  position: relative;
  border: 1px solid #e0e0e0;
}

.virtual-list-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.virtual-list-content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}

.virtual-list-item {
  padding: 0 16px;
  border-bottom: 1px solid #f0f0f0;
  box-sizing: border-box;
}
</style>

4.2 使用示例

在业务组件中使用虚拟列表:

<template>
  <div class="business-list">
    <VirtualList 
      :list-data="bigDataList"
      :item-height="60"
      :buffer-size="10"
    />
  </div>
</template>

<script>
import VirtualList from '@/components/VirtualList.vue';

export default {
  name: 'BusinessList',
  components: {
    VirtualList
  },
  data() {
    return {
      bigDataList: this.generateTestData(10000)
    };
  },
  methods: {
    generateTestData(count) {
      return Array.from({ length: count }, (_, index) => ({
        id: index + 1,
        content: `列表项 ${index + 1} - 这是测试内容用于展示虚拟滚动效果`,
        timestamp: Date.now() + index
      }));
    }
  }
};
</script>

5. vue-tui 高级特性实战

5.1 动态高度支持

固定高度的虚拟滚动相对简单,但实际项目中往往需要支持动态高度。vue-tui 通过位置缓存算法解决这个问题:

<template>
  <div class="dynamic-virtual-list">
    <!-- 基础容器结构同上 -->
    <div 
      v-for="item in visibleData"
      :key="item.id"
      ref="items"
      class="dynamic-item"
      @vue:mounted="cacheItemSize(item)"
    >
      <div class="item-content">
        <h4>{{ item.title }}</h4>
        <p>{{ item.description }}</p>
        <div v-if="item.image" class="item-image">
          <img :src="item.image" :alt="item.title">
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  // ... 其他选项同上
  data() {
    return {
      sizeCache: new Map(),  // 尺寸缓存
      estimatedItemHeight: 80, // 预估高度
      positions: []           // 位置信息数组
    };
  },
  methods: {
    cacheItemSize(item) {
      const element = this.$refs.items.find(ref => ref.dataset.id === item.id);
      if (element) {
        const height = element.offsetHeight;
        this.sizeCache.set(item.id, height);
        this.updatePositions();
      }
    },
    updatePositions() {
      let top = 0;
      this.positions = this.listData.map(item => {
        const height = this.sizeCache.get(item.id) || this.estimatedItemHeight;
        const position = { top, height, bottom: top + height };
        top = position.bottom;
        return position;
      });
      
      // 更新总高度
      this.totalHeight = top;
    },
    calculateDynamicRange(scrollTop) {
      // 二分查找找到起始位置
      this.visibleStartIndex = this.binarySearch(scrollTop);
      this.visibleEndIndex = this.binarySearch(scrollTop + this.containerHeight);
    },
    binarySearch(offset) {
      let start = 0;
      let end = this.positions.length - 1;
      let index = -1;
      
      while (start <= end) {
        const mid = Math.floor((start + end) / 2);
        const midPosition = this.positions[mid];
        
        if (midPosition.bottom >= offset) {
          if (midPosition.top <= offset) {
            index = mid;
            break;
          }
          end = mid - 1;
        } else {
          start = mid + 1;
        }
      }
      
      return index === -1 ? 0 : index;
    }
  }
};
</script>

5.2 水平虚拟滚动

除了垂直滚动,vue-tui 还支持水平虚拟滚动,特别适合宽表格场景:

<template>
  <div 
    class="horizontal-virtual-scroll"
    @scroll="handleHorizontalScroll"
    ref="horizontalContainer"
  >
    <div class="horizontal-phantom" :style="horizontalPhantomStyle"></div>
    <div class="horizontal-content" :style="horizontalContentStyle">
      <div 
        v-for="column in visibleColumns"
        :key="column.id"
        class="virtual-column"
        :style="getColumnStyle(column)"
      >
        <div class="column-header">{{ column.title }}</div>
        <div 
          v-for="item in visibleData"
          :key="item.id"
          class="column-cell"
        >
          {{ item[column.field] }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      visibleStartColumn: 0,
      visibleEndColumn: 0,
      containerWidth: 0,
      columnWidth: 120
    };
  },
  computed: {
    horizontalPhantomStyle() {
      return {
        width: this.columns.length * this.columnWidth + 'px',
        height: '100%'
      };
    },
    horizontalContentStyle() {
      return {
        transform: `translateX(${this.visibleStartColumn * this.columnWidth}px)`
      };
    },
    visibleColumns() {
      return this.columns.slice(
        Math.max(0, this.visibleStartColumn - 2),
        Math.min(this.columns.length, this.visibleEndColumn + 2)
      );
    }
  },
  methods: {
    handleHorizontalScroll(event) {
      const scrollLeft = event.target.scrollLeft;
      this.calculateHorizontalRange(scrollLeft);
    },
    calculateHorizontalRange(scrollLeft) {
      this.visibleStartColumn = Math.floor(scrollLeft / this.columnWidth);
      this.visibleEndColumn = this.visibleStartColumn + 
        Math.ceil(this.containerWidth / this.columnWidth);
    },
    getColumnStyle(column) {
      return {
        width: this.columnWidth + 'px'
      };
    }
  }
};
</script>

6. 性能优化与最佳实践

6.1 内存管理策略

虚拟滚动虽然减少了 DOM 数量,但不当的内存管理仍会导致性能问题:

// 内存管理工具类
class VirtualScrollMemoryManager {
  constructor() {
    this.cache = new Map();
    this.maxCacheSize = 1000;
    this.cleanupThreshold = 100;
  }
  
  // 缓存项数据
  cacheItem(id, data, timestamp) {
    if (this.cache.size >= this.maxCacheSize) {
      this.cleanupOldItems();
    }
    
    this.cache.set(id, {
      data,
      timestamp,
      lastAccess: Date.now()
    });
  }
  
  // 清理旧缓存
  cleanupOldItems() {
    const now = Date.now();
    const toDelete = [];
    
    for (const [id, item] of this.cache) {
      if (now - item.lastAccess > 30000) { // 30秒未访问
        toDelete.push(id);
      }
    }
    
    toDelete.forEach(id => this.cache.delete(id));
  }
  
  // 获取缓存项
  getCachedItem(id) {
    const item = this.cache.get(id);
    if (item) {
      item.lastAccess = Date.now();
    }
    return item?.data;
  }
}

6.2 滚动性能优化

// 高性能滚动处理
class ScrollPerformanceOptimizer {
  constructor() {
    this.lastScrollTop = 0;
    this.lastScrollTime = 0;
    this.animationFrameId = null;
    this.scrollThrottleDelay = 16; // ~60fps
  }
  
  // 节流滚动处理
  throttledScrollHandler(callback) {
    return (event) => {
      const currentTime = Date.now();
      const scrollTop = event.target.scrollTop;
      
      // 快速滚动时降低更新频率
      if (currentTime - this.lastScrollTime > this.scrollThrottleDelay ||
          Math.abs(scrollTop - this.lastScrollTop) > 100) {
        
        if (this.animationFrameId) {
          cancelAnimationFrame(this.animationFrameId);
        }
        
        this.animationFrameId = requestAnimationFrame(() => {
          callback(event);
          this.lastScrollTime = currentTime;
          this.lastScrollTop = scrollTop;
        });
      }
    };
  }
  
  // 平滑滚动到指定位置
  smoothScrollTo(element, targetPosition, duration = 300) {
    const startPosition = element.scrollTop;
    const distance = targetPosition - startPosition;
    const startTime = performance.now();
    
    const animateScroll = (currentTime) => {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);
      
      // 缓动函数
      const ease = progress < 0.5 
        ? 2 * progress * progress 
        : -1 + (4 - 2 * progress) * progress;
      
      element.scrollTop = startPosition + distance * ease;
      
      if (progress < 1) {
        requestAnimationFrame(animateScroll);
      }
    };
    
    requestAnimationFrame(animateScroll);
  }
}

7. 实际项目集成示例

7.1 大型数据表格实战

结合虚拟滚动实现高性能数据表格:

<template>
  <div class="virtual-data-table">
    <div class="table-header">
      <div 
        v-for="column in visibleColumns"
        :key="column.key"
        class="header-cell"
        :style="{ width: column.width + 'px' }"
      >
        {{ column.title }}
      </div>
    </div>
    
    <div 
      class="table-body"
      @scroll="handleTableScroll"
      ref="tableBody"
    >
      <div class="body-phantom" :style="bodyPhantomStyle"></div>
      <div class="body-content" :style="bodyContentStyle">
        <div 
          v-for="row in visibleRows"
          :key="row.id"
          class="table-row"
        >
          <div 
            v-for="column in visibleColumns"
            :key="column.key"
            class="body-cell"
            :style="{ width: column.width + 'px' }"
          >
            {{ row[column.key] }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    columns: Array,
    data: Array,
    rowHeight: {
      type: Number,
      default: 48
    }
  },
  data() {
    return {
      visibleStartRow: 0,
      visibleEndRow: 0,
      visibleStartCol: 0,
      visibleEndCol: 0,
      bodyHeight: 0,
      bodyWidth: 0
    };
  },
  computed: {
    bodyPhantomStyle() {
      return {
        height: this.data.length * this.rowHeight + 'px',
        width: this.totalColumnsWidth + 'px'
      };
    },
    bodyContentStyle() {
      return {
        transform: `translateY(${this.visibleStartRow * this.rowHeight}px)`
      };
    },
    visibleRows() {
      return this.data.slice(this.visibleStartRow, this.visibleEndRow + 1);
    },
    visibleColumns() {
      return this.columns.slice(this.visibleStartCol, this.visibleEndCol + 1);
    },
    totalColumnsWidth() {
      return this.columns.reduce((sum, col) => sum + col.width, 0);
    }
  },
  mounted() {
    this.initTableDimensions();
    this.calculateVisibleRanges();
  },
  methods: {
    initTableDimensions() {
      const bodyElement = this.$refs.tableBody;
      this.bodyHeight = bodyElement.clientHeight;
      this.bodyWidth = bodyElement.clientWidth;
    },
    handleTableScroll(event) {
      const scrollTop = event.target.scrollTop;
      const scrollLeft = event.target.scrollLeft;
      this.calculateVisibleRanges(scrollTop, scrollLeft);
    },
    calculateVisibleRanges(scrollTop = 0, scrollLeft = 0) {
      // 计算可见行范围
      this.visibleStartRow = Math.floor(scrollTop / this.rowHeight);
      this.visibleEndRow = this.visibleStartRow + 
        Math.ceil(this.bodyHeight / this.rowHeight);
      
      // 计算可见列范围
      let accumulatedWidth = 0;
      this.visibleStartCol = 0;
      this.visibleEndCol = this.columns.length - 1;
      
      for (let i = 0; i < this.columns.length; i++) {
        accumulatedWidth += this.columns[i].width;
        if (accumulatedWidth > scrollLeft && this.visibleStartCol === 0) {
          this.visibleStartCol = Math.max(0, i - 1);
        }
        if (accumulatedWidth > scrollLeft + this.bodyWidth) {
          this.visibleEndCol = i + 1;
          break;
        }
      }
    }
  }
};
</script>

7.2 无限滚动加载

结合分页加载实现真正的无限滚动:

// 无限滚动数据管理器
class InfiniteScrollManager {
  constructor() {
    this.currentPage = 1;
    this.isLoading = false;
    this.hasMore = true;
    this.loadedPages = new Set();
  }
  
  async loadNextPage() {
    if (this.isLoading || !this.hasMore) return;
    
    this.isLoading = true;
    try {
      const response = await this.fetchPageData(this.currentPage);
      this.handleNewData(response);
      this.currentPage++;
      this.loadedPages.add(this.currentPage);
    } catch (error) {
      console.error('加载数据失败:', error);
    } finally {
      this.isLoading = false;
    }
  }
  
  handleNewData(response) {
    const { data, total, currentPage } = response;
    
    // 判断是否还有更多数据
    this.hasMore = data.length > 0 && currentPage * data.length < total;
    
    // 触发数据更新
    this.onNewData(data);
  }
  
  // 检查是否需要预加载下一页
  checkPreload(scrollTop, containerHeight, totalHeight) {
    const scrollRatio = scrollTop / totalHeight;
    const preloadThreshold = 0.8; // 滚动到80%时预加载
    
    if (scrollRatio > preloadThreshold && !this.isLoading && this.hasMore) {
      this.loadNextPage();
    }
  }
}

8. 常见问题与解决方案

8.1 滚动闪烁问题

问题现象 :快速滚动时出现内容闪烁或跳动 根本原因 :渲染延迟导致可见区域计算不准确 解决方案 :

// 双缓冲渲染策略
class DoubleBufferRenderer {
  constructor() {
    this.visibleBuffer = [];
    this.nextBuffer = [];
    this.isSwapping = false;
  }
  
  // 准备下一帧数据
  prepareNextFrame(newVisibleData) {
    this.nextBuffer = newVisibleData;
    
    if (!this.isSwapping) {
      this.swapBuffers();
    }
  }
  
  // 交换缓冲区
  swapBuffers() {
    this.isSwapping = true;
    
    requestAnimationFrame(() => {
      this.visibleBuffer = this.nextBuffer;
      this.nextBuffer = [];
      this.isSwapping = false;
      
      // 触发视图更新
      this.onBufferSwapped();
    });
  }
}

8.2 动态内容高度变化

问题现象 :内容加载后高度变化,导致滚动位置错乱 解决方案 :

// 动态高度管理器
class DynamicHeightManager {
  constructor() {
    this.heightCache = new Map();
    this.observer = null;
  }
  
  // 监听元素高度变化
  observeHeightChanges(element, itemId) {
    if (!this.observer) {
      this.observer = new ResizeObserver(entries => {
        entries.forEach(entry => {
          const id = entry.target.dataset.itemId;
          const newHeight = entry.contentRect.height;
          this.updateHeightCache(id, newHeight);
        });
      });
    }
    
    this.observer.observe(element);
  }
  
  updateHeightCache(id, height) {
    const oldHeight = this.heightCache.get(id);
    if (oldHeight !== height) {
      this.heightCache.set(id, height);
      this.onHeightChanged(id, height, oldHeight);
    }
  }
}

8.3 移动端兼容性问题

问题现象 :在移动设备上滚动不流畅或响应迟钝 优化方案 :

/* 移动端优化样式 */
.virtual-list-mobile {
  -webkit-overflow-scrolling: touch; /* 启用惯性滚动 */
  overflow-scrolling: touch;
  backface-visibility: hidden;
  transform: translateZ(0); /* 触发硬件加速 */
}

/* 减少移动端内存占用 */
.mobile-virtual-item {
  will-change: transform;
  contain: layout style paint; /* 包含优化 */
}

9. 性能监控与调试

9.1 性能指标监控

实现虚拟滚动性能监控面板:

class VirtualScrollMonitor {
  constructor() {
    this.metrics = {
      frameRate: 0,
      memoryUsage: 0,
      renderTime: 0,
      visibleItemCount: 0
    };
    
    this.startMonitoring();
  }
  
  startMonitoring() {
    // 帧率监控
    this.monitorFrameRate();
    
    // 内存监控
    this.monitorMemory();
    
    // 渲染时间监控
    this.monitorRenderTime();
  }
  
  monitorFrameRate() {
    let frameCount = 0;
    let lastTime = performance.now();
    
    const checkFrameRate = () => {
      frameCount++;
      const currentTime = performance.now();
      
      if (currentTime - lastTime >= 1000) {
        this.metrics.frameRate = Math.round(
          (frameCount * 1000) / (currentTime - lastTime)
        );
        frameCount = 0;
        lastTime = currentTime;
      }
      
      requestAnimationFrame(checkFrameRate);
    };
    
    checkFrameRate();
  }
  
  logPerformance() {
    console.table({
      '帧率 (FPS)': this.metrics.frameRate,
      '可见项目数': this.metrics.visibleItemCount,
      '渲染时间 (ms)': this.metrics.renderTime.toFixed(2),
      '内存使用 (MB)': (this.metrics.memoryUsage / 1024 / 1024).toFixed(2)
    });
  }
}

9.2 调试工具集成

开发环境下的调试支持:

// 虚拟滚动调试工具
class VirtualScrollDebugger {
  constructor(virtualScrollInstance) {
    this.instance = virtualScrollInstance;
    this.debugOverlay = null;
    this.isDebugging = false;
  }
  
  toggleDebugOverlay() {
    if (this.isDebugging) {
      this.hideDebugOverlay();
    } else {
      this.showDebugOverlay();
    }
  }
  
  showDebugOverlay() {
    this.debugOverlay = document.createElement('div');
    this.debugOverlay.style.cssText = `
      position: fixed;
      top: 10px;
      right: 10px;
      background: rgba(0,0,0,0.8);
      color: white;
      padding: 10px;
      border-radius: 4px;
      font-family: monospace;
      z-index: 10000;
    `;
    
    document.body.appendChild(this.debugOverlay);
    this.isDebugging = true;
    this.updateDebugInfo();
  }
  
  updateDebugInfo() {
    if (!this.isDebugging) return;
    
    const info = `
可见范围: ${this.instance.visibleStartIndex} - ${this.instance.visibleEndIndex}
总项目数: ${this.instance.totalCount}
渲染项目: ${this.instance.visibleData.length}
滚动位置: ${this.instance.scrollTop}
    `.trim();
    
    this.debugOverlay.textContent = info;
    requestAnimationFrame(() => this.updateDebugInfo());
  }
}

vue-tui 的虚拟滚动方案通过精心的架构设计和多项优化技术,确实能够实现"超丝滑"的滚动体验。但在实际项目中,需要根据具体业务场景进行调优和定制。建议在开发过程中持续监控性能指标,确保在各种边界情况下都能保持流畅的用户体验。

对于需要处理超大数据量的项目,可以考虑结合 Web Worker 进行数据预处理,或者采用分片加载策略进一步优化性能。虚拟滚动只是性能优化的一个环节,完整的优化方案还需要考虑数据缓存、图片懒加载、代码分割等多个方面。

更多推荐