JavaScript 性能优化系列(四):应用响应速度优化——多页面切换场景的流畅体验-1

引言:多页面切换为何成为性能瓶颈?

在现代Web应用中,用户体验的流畅度很大程度上取决于页面切换的响应速度。研究表明,页面切换延迟超过300ms就会被用户感知为"卡顿",而超过1秒的切换延迟会导致近40%的用户放弃当前操作。

多页面应用(MPA)和单页面应用(SPA)在页面切换机制上存在本质区别:

  • 多页面应用(MPA):通过浏览器导航切换页面,每次切换都会重新请求HTML、CSS、JavaScript等资源,触发完整的页面解析和渲染流程;
  • 单页面应用(SPA):通过路由机制在同一HTML文档内切换视图,避免了完整的页面重新加载,但仍需处理组件卸载、加载、渲染等过程。

无论是MPA还是SPA,页面切换都涉及三个关键性能环节:资源加载、状态处理和视图渲染。本文作为JavaScript性能优化系列的第四篇,将深入探讨四大核心策略——路由优化、组件复用、状态管理优化和虚拟滚动,帮助你在多页面切换场景下实现"瞬时响应"的用户体验。

4.1 路由优化:路由懒加载与缓存策略

路由是多页面应用的核心基础设施,路由处理的效率直接决定了页面切换的响应速度。路由优化主要围绕两个核心目标:减少初始加载资源体积加速重复页面访问,对应的技术手段分别是路由懒加载和路由缓存。

4.1.1 原理:路由优化的底层逻辑

4.1.1.1 路由懒加载的工作原理

传统路由模式下,应用会在初始加载时打包所有页面组件和相关逻辑,导致:

  • 初始加载资源体积过大,首屏时间延长;
  • 即使用户永远不会访问某些页面,相关资源也会被加载,造成带宽浪费。

路由懒加载(Route Lazy Loading)基于"按需加载"思想,核心原理是:

  • 将不同路由对应的组件分割成不同的代码块(Chunk);
  • 仅在用户访问特定路由时,才动态加载该路由对应的代码块;
  • 结合现代浏览器的import()函数(ES6模块动态导入)实现代码分割和异步加载。

这种方式能显著减少应用初始加载的资源体积,研究表明,合理应用路由懒加载可使初始JS包体积减少60%-80%,首屏加载时间缩短40%以上。

4.1.1.2 路由缓存的作用机制

当用户在多个页面间频繁切换时(如电商应用的列表页和详情页),重复加载相同资源会导致:

  • 不必要的网络请求,延长页面切换时间;
  • 重复的组件初始化和数据请求,浪费CPU资源。

路由缓存策略通过存储和复用已加载的资源和状态,避免重复工作,主要包括:

  • 组件缓存:保留已渲染组件的DOM结构和实例,避免重复创建和销毁;
  • 数据缓存:存储已请求的数据,避免重复的API调用;
  • 资源缓存:利用浏览器缓存机制(如HTTP缓存、Service Worker)缓存静态资源。

有效的路由缓存可使重复页面访问的响应时间缩短50%-90%,显著提升多页面切换的流畅度。

4.1.2 代码样例:主流框架的路由优化实现

4.1.2.1 React路由优化(基于React Router 6)
// 符合谷歌JavaScript规范:清晰的导入顺序,明确的类型注释
import React, { Suspense, lazy, useState } from 'react';
import { BrowserRouter as Router, Routes, Route, NavLink, useLocation, useNavigate } from 'react-router-dom';
import { Spin, Layout, Menu, Typography } from 'antd'; // 假设使用Ant Design组件库

// 1. 公共组件(初始加载)
const Loading = () => (
  <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '300px' }}>
    <Spin size="large" tip="加载中..." />
  </div>
);

const Header = () => {
  const location = useLocation();
  const navigate = useNavigate();
  
  // 导航项配置
  const menuItems = [
    { key: '/', label: '首页' },
    { key: '/products', label: '产品列表' },
    { key: '/about', label: '关于我们' },
    { key: '/contact', label: '联系我们' },
    { key: '/user/profile', label: '个人中心' },
    { key: '/user/orders', label: '我的订单' },
  ];
  
  return (
    <Layout.Header style={{ display: 'flex', justifyContent: 'space-between' }}>
      <Typography.Title level={3} style={{ margin: 0, color: 'white' }}>
        示例应用
      </Typography.Title>
      <Menu
        items={menuItems}
        selectedKeys={[location.pathname]}
        mode="horizontal"
        theme="dark"
        onClick={({ key }) => navigate(key)}
      />
    </Layout.Header>
  );
};

// 2. 路由懒加载配置(按路由分组)
// 首页组件(初始加载,优先级最高)
import Home from './pages/Home';

// 其他路由组件(懒加载)
const Products = lazy(() => import('./pages/Products'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

// 用户相关路由(按模块分组加载)
const UserModule = lazy(() => import(/* webpackChunkName: "user" */ './pages/user'));
// 注意:UserModule 内部应包含 Profile 和 Orders 组件的路由配置

// 3. 路由缓存高阶组件
const withRouteCache = (Component) => {
  // 缓存组件实例的Map
  const componentCache = new Map();
  
  const CachedComponent = (props) => {
    const { pathname } = useLocation();
    const [cacheKey] = useState(pathname); // 以路径作为缓存键
    
    // 如果缓存中没有,创建新实例并缓存
    if (!componentCache.has(cacheKey)) {
      componentCache.set(cacheKey, <Component {...props} />);
    }
    
    // 返回缓存的组件实例
    return componentCache.get(cacheKey);
  };
  
  return CachedComponent;
};

// 4. 应用路由配置
const AppRouter = () => (
  <Router>
    <Layout style={{ minHeight: '100vh' }}>
      <Header />
      <Layout.Content style={{ padding: '20px' }}>
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/" element={<Home />} />
            {/* 产品列表页面使用缓存 */}
            <Route path="/products" element={<withRouteCache(Products) />} />
            <Route path="/about" element={<About />} />
            <Route path="/contact" element={<Contact />} />
            
            {/* 用户模块路由 */}
            <Route path="/user/*" element={<UserModule />} />
            
            {/* 404页面 */}
            <Route path="*" element={<Typography.Text>页面未找到</Typography.Text>} />
          </Routes>
        </Suspense>
      </Layout.Content>
    </Layout>
  </Router>
);

export default AppRouter;
// pages/user/index.js - 用户模块路由(按模块分组)
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Tabs } from 'antd';
import { withRouteCache } from '../../utils/withRouteCache';

// 模块内部组件(懒加载)
const Profile = React.lazy(() => import('./Profile'));
const Orders = React.lazy(() => import('./Orders'));

// 用户模块容器组件
const UserModule = () => {
  // 模拟用户登录状态检查
  const isAuthenticated = true; // 实际项目中应从状态管理中获取
  
  // 如果未登录,重定向到登录页
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  
  return (
    <div>
      <h2>用户中心</h2>
      <Tabs defaultActiveKey="profile">
        <Tabs.TabPane tab="个人资料" key="profile">
          <React.Suspense fallback={<div>加载个人资料...</div>}>
            <Profile />
          </React.Suspense>
        </Tabs.TabPane>
        <Tabs.TabPane tab="我的订单" key="orders">
          <React.Suspense fallback={<div>加载订单列表...</div>}>
            <withRouteCache(Orders) /> {/* 订单页面使用缓存 */}
          </React.Suspense>
        </Tabs.TabPane>
      </Tabs>
    </div>
  );
};

export default UserModule;
4.1.2.2 Vue路由优化(基于Vue Router 4)
// router/index.js - Vue路由配置(符合谷歌JavaScript规范)
import { createRouter, createWebHistory } from 'vue-router';

// 1. 导入公共组件
import MainLayout from '../layouts/MainLayout.vue';
import Loading from '../components/Loading.vue';

// 2. 路由配置 - 使用路由懒加载和代码分割
const routes = [
  {
    path: '/',
    component: MainLayout,
    children: [
      // 首页 - 优先加载
      {
        path: '',
        name: 'Home',
        component: () => import('../views/Home.vue'),
        meta: {
          cache: false, // 首页不缓存,每次访问刷新
          title: '首页'
        }
      },
      // 产品列表 - 懒加载并缓存
      {
        path: '/products',
        name: 'Products',
        component: () => import(/* webpackChunkName: "products" */ '../views/Products.vue'),
        meta: {
          cache: true, // 缓存该路由组件
          keepAlive: true,
          title: '产品列表'
        }
      },
      // 产品详情 - 懒加载,按ID缓存
      {
        path: '/products/:id',
        name: 'ProductDetail',
        component: () => import(/* webpackChunkName: "products" */ '../views/ProductDetail.vue'),
        meta: {
          cache: true,
          keepAlive: true,
          title: '产品详情',
          // 自定义缓存键生成函数,基于路由参数
          getCacheKey: (route) => `product_${route.params.id}`
        }
      },
      // 关于我们 - 懒加载
      {
        path: '/about',
        name: 'About',
        component: () => import('../views/About.vue'),
        meta: {
          cache: false,
          title: '关于我们'
        }
      },
      // 用户模块路由分组
      {
        path: '/user',
        children: [
          {
            path: 'profile',
            name: 'UserProfile',
            component: () => import(/* webpackChunkName: "user" */ '../views/user/Profile.vue'),
            meta: {
              cache: true,
              requiresAuth: true,
              title: '个人资料'
            }
          },
          {
            path: 'orders',
            name: 'UserOrders',
            component: () => import(/* webpackChunkName: "user" */ '../views/user/Orders.vue'),
            meta: {
              cache: true,
              requiresAuth: true,
              title: '我的订单'
            }
          }
        ]
      }
    ]
  },
  // 登录页 - 单独布局
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue'),
    meta: {
      layout: 'empty', // 使用空布局
      title: '登录'
    }
  },
  // 404页面
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFound.vue'),
    meta: {
      title: '页面未找到'
    }
  }
];

// 3. 创建路由实例
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
  // 滚动行为优化:切换路由时滚动到顶部
  scrollBehavior(to, from, savedPosition) {
    // 如果有保存的位置(如后退导航),恢复位置
    if (savedPosition) {
      return savedPosition;
    }
    // 否则滚动到顶部
    return { top: 0 };
  }
});

// 4. 路由拦截器 - 实现缓存和权限控制
router.beforeEach(async (to, from, next) => {
  // 设置页面标题
  document.title = to.meta.title || '示例应用';
  
  // 权限控制:需要登录的路由
  if (to.meta.requiresAuth) {
    const isAuthenticated = await checkAuthStatus(); // 检查登录状态
    if (!isAuthenticated) {
      // 未登录,重定向到登录页,并记录目标地址
      return next({ name: 'Login', query: { redirect: to.fullPath } });
    }
  }
  
  next();
});

// 5. 检查登录状态的辅助函数
async function checkAuthStatus() {
  // 实际项目中应调用API或检查本地存储的认证信息
  return new Promise(resolve => {
    // 模拟异步检查
    setTimeout(() => {
      const token = localStorage.getItem('auth_token');
      resolve(!!token);
    }, 100);
  });
}

export default router;
<!-- App.vue - 实现Vue组件缓存机制 -->
<template>
  <component :is="layoutComponent">
    <!-- 缓存路由组件 -->
    <keep-alive :include="cachedComponents" :max="10"> <!-- 限制最大缓存数量 -->
      <router-view v-slot="{ Component }">
        <template v-if="Component">
          <Component :key="getComponentKey($route)"/>
        </template>
        <template v-else>
          <Loading />
        </template>
      </router-view>
    </keep-alive>
  </component>
</template>

<script>
import { computed, watchEffect, ref } from 'vue';
import { useRoute } from 'vue-router';
import MainLayout from './layouts/MainLayout.vue';
import EmptyLayout from './layouts/EmptyLayout.vue';
import Loading from './components/Loading.vue';

export default {
  name: 'App',
  components: {
    Loading
  },
  setup() {
    const route = useRoute();
    const cachedComponents = ref([]);
    
    // 确定当前使用的布局
    const layoutComponent = computed(() => {
      return route.meta.layout === 'empty' ? EmptyLayout : MainLayout;
    });
    
    // 生成组件缓存键
    const getComponentKey = (route) => {
      // 如果路由配置了自定义缓存键生成函数,使用它
      if (typeof route.meta.getCacheKey === 'function') {
        return route.meta.getCacheKey(route);
      }
      // 否则使用路由名称或路径作为缓存键
      return route.name || route.path;
    };
    
    // 管理缓存组件列表
    watchEffect(() => {
      const currentKey = getComponentKey(route);
      // 如果路由需要缓存且不在缓存列表中,添加到缓存
      if (route.meta.cache && !cachedComponents.value.includes(currentKey)) {
        // 如果缓存数量达到上限,移除最早的一个
        if (cachedComponents.value.length >= 10) {
          cachedComponents.value.shift();
        }
        cachedComponents.value.push(currentKey);
      }
    });
    
    return {
      layoutComponent,
      cachedComponents,
      getComponentKey
    };
  }
};
</script>
4.1.2.3 数据缓存策略实现
// services/apiCache.js - API数据缓存服务(通用实现)
class ApiCache {
  /**
   * 初始化缓存服务
   * @param {Object} options - 缓存配置
   * @param {number} options.defaultTTL - 默认缓存过期时间(毫秒)
   * @param {number} options.maxSize - 最大缓存条目数
   */
  constructor({ defaultTTL = 5 * 60 * 1000, maxSize = 100 } = {}) {
    this.cache = new Map(); // 存储缓存数据
    this.defaultTTL = defaultTTL; // 默认缓存5分钟
    this.maxSize = maxSize; // 最大缓存100条数据
  }
  
  /**
   * 生成缓存键
   * @param {string} url - 请求URL
   * @param {Object} params - 请求参数
   * @returns {string} 缓存键
   */
  generateKey(url, params = {}) {
    // 对参数进行排序,确保相同参数不同顺序生成相同的键
    const sortedParams = Object.keys(params).sort().reduce((obj, key) => {
      obj[key] = params[key];
      return obj;
    }, {});
    
    return `${url}?${JSON.stringify(sortedParams)}`;
  }
  
  /**
   * 检查缓存是否有效
   * @param {string} key - 缓存键
   * @returns {boolean} 是否有效
   */
  isCacheValid(key) {
    if (!this.cache.has(key)) {
      return false;
    }
    
    const { timestamp, ttl } = this.cache.get(key);
    const now = Date.now();
    
    // 检查是否过期
    return now - timestamp < ttl;
  }
  
  /**
   * 获取缓存数据
   * @param {string} url - 请求URL
   * @param {Object} params - 请求参数
   * @returns {any|null} 缓存数据或null
   */
  getCache(url, params = {}) {
    const key = this.generateKey(url, params);
    
    if (this.isCacheValid(key)) {
      return this.cache.get(key).data;
    }
    
    // 缓存无效,移除它
    this.cache.delete(key);
    return null;
  }
  
  /**
   * 设置缓存数据
   * @param {string} url - 请求URL
   * @param {Object} params - 请求参数
   * @param {any} data - 要缓存的数据
   * @param {number} ttl - 缓存过期时间(毫秒,可选)
   */
  setCache(url, params = {}, data, ttl = this.defaultTTL) {
    const key = this.generateKey(url, params);
    
    // 如果缓存达到最大数量,移除最旧的条目
    if (this.cache.size >= this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl
    });
  }
  
  /**
   * 清除指定缓存
   * @param {string} url - 请求URL
   * @param {Object} params - 请求参数(可选)
   */
  clearCache(url, params) {
    if (url && params) {
      const key = this.generateKey(url, params);
      this.cache.delete(key);
    } else if (url) {
      // 清除该URL的所有相关缓存
      const keysToDelete = [];
      this.cache.forEach((_, key) => {
        if (key.startsWith(url)) {
          keysToDelete.push(key);
        }
      });
      keysToDelete.forEach(key => this.cache.delete(key));
    } else {
      // 清除所有缓存
      this.cache.clear();
    }
  }
  
  /**
   * 带缓存的请求函数
   * @param {Function} fetchFn - 实际的请求函数
   * @param {string} url - 请求URL
   * @param {Object} params - 请求参数
   * @param {number} ttl - 缓存过期时间
   * @returns {Promise<any>} 请求结果
   */
  async requestWithCache(fetchFn, url, params = {}, ttl) {
    // 尝试从缓存获取
    const cachedData = this.getCache(url, params);
    if (cachedData) {
      // 从缓存获取,用setTimeout确保异步行为一致
      return new Promise(resolve => {
        setTimeout(() => resolve(cachedData), 0);
      });
    }
    
    // 缓存未命中,执行实际请求
    try {
      const data = await fetchFn(url, params);
      // 缓存请求结果
      this.setCache(url, params, data, ttl);
      return data;
    } catch (error) {
      console.error('请求失败:', error);
      throw error;
    }
  }
}

// 创建缓存实例并导出
export const apiCache = new ApiCache({
  defaultTTL: 5 * 60 * 1000, // 默认缓存5分钟
  maxSize: 50 // 最大缓存50条数据
});
// services/productService.js - 产品服务(使用缓存)
import { apiCache } from './apiCache';

// 实际的API请求函数
async function fetchProductsFromApi(url, params) {
  const queryParams = new URLSearchParams(params);
  const response = await fetch(`${url}?${queryParams}`);
  
  if (!response.ok) {
    throw new Error(`请求失败: ${response.status}`);
  }
  
  return response.json();
}

// 产品服务
export const productService = {
  /**
   * 获取产品列表(带缓存)
   * @param {Object} params - 查询参数
   * @returns {Promise<Array>} 产品列表
   */
  async getProducts(params = { page: 1, limit: 10 }) {
    return apiCache.requestWithCache(
      fetchProductsFromApi,
      '/api/products',
      params,
      60 * 1000 // 产品列表缓存1分钟
    );
  },
  
  /**
   * 获取产品详情(带缓存)
   * @param {string} id - 产品ID
   * @returns {Promise<Object>} 产品详情
   */
  async getProductDetail(id) {
    return apiCache.requestWithCache(
      fetchProductsFromApi,
      `/api/products/${id}`,
      {},
      5 * 60 * 1000 // 产品详情缓存5分钟
    );
  },
  
  /**
   * 更新产品信息(会清除相关缓存)
   * @param {string} id - 产品ID
   * @param {Object} data - 产品数据
   * @returns {Promise<Object>} 更新后的产品
   */
  async updateProduct(id, data) {
    const response = await fetch(`/api/products/${id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      throw new Error(`更新失败: ${response.status}`);
    }
    
    const updatedProduct = await response.json();
    
    // 清除该产品的缓存
    apiCache.clearCache(`/api/products/${id}`);
    // 清除产品列表缓存,确保下次能获取最新数据
    apiCache.clearCache('/api/products');
    
    return updatedProduct;
  }
};

4.1.3 实践反例:路由优化中的常见错误

4.1.3.1 反例1:过度分割或不当分组
// 错误:路由分割过细,导致请求过多
const routes = [
  {
    path: '/products',
    name: 'Products',
    // 错误:每个小组件都单独分割,增加网络请求次数
    component: () => import('./views/Products/ProductsContainer.vue'),
    children: [
      {
        path: '',
        component: () => import('./views/Products/ProductList.vue')
      },
      {
        path: ':id',
        component: () => import('./views/Products/ProductDetail.vue')
      },
      {
        path: ':id/reviews',
        component: () => import('./views/Products/ProductReviews.vue')
      },
      {
        path: ':id/specs',
        component: () => import('./views/Products/ProductSpecs.vue')
      }
    ]
  }
];

问题:路由分割过细会导致页面切换时产生过多的网络请求,每个请求都有额外的开销(如TCP握手、TLS协商),反而会延长页面加载时间。研究表明,在HTTP/1.1环境下,并行请求数有限制(通常6个),过多的小请求会导致排队等待。

4.1.3.2 反例2:缓存策略不当导致数据不一致
// 错误:缓存策略不当,导致数据不一致
const ProductDetail = () => {
  const { id } = useParams();
  const [product, setProduct] = useState(null);
  
  useEffect(() => {
    // 错误:无条件使用缓存,不考虑数据时效性
    const cached = localStorage.getItem(`product_${id}`);
    if (cached) {
      setProduct(JSON.parse(cached));
      return;
    }
    
    // 缓存未命中时才请求
    fetch(`/api/products/${id}`)
      .then(res => res.json())
      .then(data => {
        setProduct(data);
        // 错误:设置永久缓存,没有过期机制
        localStorage.setItem(`product_${id}`, JSON.stringify(data));
      });
  }, [id]);
  
  // 错误:更新产品后没有清除缓存
  const handleUpdate = async (updatedData) => {
    await fetch(`/api/products/${id}`, {
      method: 'PUT',
      body: JSON.stringify(updatedData)
    });
    // 只更新了本地状态,没有更新或清除缓存
    setProduct({ ...product, ...updatedData });
  };
  
  return <div>{/* 产品详情渲染 */}</div>;
};

问题:无条件使用永久缓存会导致用户看到过时数据,尤其是在数据可能被频繁更新的场景(如商品价格、库存)。当数据更新后,如果没有相应的缓存更新或清除机制,会造成客户端数据与服务器数据不一致,严重影响用户体验。

4.1.3.3 反例3:路由切换时的不必要重绘
// 错误:路由切换时的不必要重绘和布局偏移
// App.jsx
const App = () => {
  return (
    <div>
      <Router>
        {/* 错误:每次路由切换都会重新创建Header */}
        <Header key={Date.now()} />
        
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<Products />} />
          {/* 其他路由 */}
        </Routes>
      </Router>
    </div>
  );
};

// Header.jsx
const Header = () => {
  // 错误:每次渲染都会生成新的样式对象,导致不必要的重绘
  const headerStyle = {
    backgroundColor: '#fff',
    boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
    padding: '1rem'
  };
  
  return (
    <header style={headerStyle}>
      {/* 导航内容 */}
    </header>
  );
};

问题:路由切换时,非路由相关组件(如Header、Footer)不应该重新渲染。为这些组件添加不稳定的key(如Date.now())或在组件内部创建新的对象/函数,会导致它们在每次路由切换时重新渲染,触发不必要的DOM操作和重绘,影响页面切换性能。

4.1.3.4 反例4:未处理懒加载失败场景
// 错误:未处理路由懒加载失败的情况
const routes = [
  {
    path: '/products',
    name: 'Products',
    // 错误:没有错误处理机制
    component: () => import('./views/Products.vue')
  },
  {
    path: '/user/profile',
    name: 'UserProfile',
    // 错误:没有加载状态提示
    component: () => import('./views/user/Profile.vue')
  }
];

// 在组件中使用
const App = () => {
  return (
    <Router>
      {/* 错误:没有全局的错误边界处理懒加载失败 */}
      <Routes>
        {routes.map(route => (
          <Route key={route.path} {...route} />
        ))}
      </Routes>
    </Router>
  );
};

问题:网络不稳定时,路由懒加载可能失败。如果没有适当的错误处理和重试机制,用户会看到空白页面或不友好的错误信息。同时,缺少加载状态提示会让用户不确定页面是否正在加载,影响用户体验。

4.1.4 代码评审要点:路由优化的检查清单

评审维度检查要点工具支持
路由分割策略1. 是否按功能模块合理分组路由,避免过度分割?
2. 首屏路由是否优先加载,非首屏路由是否懒加载?
3. 路由代码分割的chunk命名是否清晰?
1. Webpack Bundle Analyzer
2. Chrome DevTools的Network面板
缓存策略有效性1. 是否为不同类型的路由设置了合理的缓存策略?
2. 缓存是否有明确的过期机制?
3. 数据更新后是否及时清除或更新相关缓存?
1. Chrome DevTools的Application面板(查看缓存)
2. 手动测试数据更新场景
加载状态处理1. 路由懒加载是否有明确的加载状态提示?
2. 是否处理了加载超时和失败的情况?
3. 加载状态UI是否友好且不影响用户体验?
1. 模拟慢速网络(Chrome DevTools的Network Throttling)
2. 模拟请求失败(Chrome DevTools的Request Blocking)
路由切换性能1. 路由切换时间是否控制在300ms以内?
2. 路由切换时是否有不必要的重绘和回流?
3. 页面滚动位置是否合理重置或保留?
1. Chrome DevTools的Performance面板
2. Lighthouse的Performance指标
错误处理机制1. 是否有全局错误边界处理路由加载错误?
2. 错误信息是否清晰且提供了重试选项?
3. 是否有降级方案应对关键路由加载失败?
1. 手动测试各种错误场景
2. 错误监控工具(如Sentry)
可访问性与用户体验1. 路由切换时是否有适当的过渡动画?
2. 加载状态是否对屏幕阅读器友好?
3. 路由变化是否正确反映在浏览器历史记录中?
1. 屏幕阅读器测试(如NVDA)
2. 键盘导航测试
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐