用户体验优化

前端常见的用户体验优化技术与实现

问题

前端开发中有哪些常见的用户体验优化手段?如何实现?

解答

1. 骨架屏

页面加载时显示占位内容,避免白屏。

/* 骨架屏样式 */
.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
// React 骨架屏组件
function Skeleton({ width, height }) {
  return (
    <div 
      className="skeleton" 
      style={{ width, height, borderRadius: 4 }}
    />
  );
}

function ArticleList({ loading, articles }) {
  if (loading) {
    return Array(3).fill(0).map((_, i) => (
      <div key={i} style={{ marginBottom: 16 }}>
        <Skeleton width="60%" height={24} />
        <Skeleton width="100%" height={16} />
        <Skeleton width="80%" height={16} />
      </div>
    ));
  }
  
  return articles.map(article => (
    <Article key={article.id} data={article} />
  ));
}

2. 防抖与节流

优化高频事件,减少不必要的计算。

// 防抖:输入停止后才执行
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 节流:固定间隔执行一次
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// 使用示例
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce(e => {
  fetchSearchResults(e.target.value);
}, 300));

window.addEventListener('scroll', throttle(() => {
  updateScrollProgress();
}, 100));

3. 图片懒加载

延迟加载视口外的图片。

// 使用 Intersection Observer
function lazyLoadImages() {
  const images = document.querySelectorAll('img[data-src]');
  
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute('data-src');
        observer.unobserve(img);
      }
    });
  }, {
    rootMargin: '50px' // 提前 50px 加载
  });
  
  images.forEach(img => observer.observe(img));
}
<!-- HTML 结构 -->
<img data-src="real-image.jpg" src="placeholder.jpg" alt="描述">

4. 按钮加载状态

防止重复提交,提供操作反馈。

function SubmitButton({ onClick, children }) {
  const [loading, setLoading] = useState(false);
  
  const handleClick = async () => {
    if (loading) return;
    
    setLoading(true);
    try {
      await onClick();
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? <Spinner /> : children}
    </button>
  );
}

5. 乐观更新

先更新 UI,再发请求,失败时回滚。

async function toggleLike(postId) {
  // 乐观更新:立即更新 UI
  const previousState = posts[postId].liked;
  updatePost(postId, { liked: !previousState });
  
  try {
    await api.toggleLike(postId);
  } catch (error) {
    // 失败回滚
    updatePost(postId, { liked: previousState });
    showToast('操作失败,请重试');
  }
}

6. 虚拟列表

只渲染可见区域的列表项。

function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);
  
  // 计算可见范围
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  );
  
  // 只渲染可见项
  const visibleItems = items.slice(startIndex, endIndex);
  
  return (
    <div 
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={e => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        {visibleItems.map((item, index) => (
          <div
            key={startIndex + index}
            style={{
              position: 'xop5g',
              top: (startIndex + index) * itemHeight,
              height: itemHeight
            }}
          >
            {item.content}
          </div>
        ))}
      </div>
    </div>
  );
}

7. 错误边界与友好提示

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <p>出了点问题</p>
          <button onClick={() => window.location.reload()}>
            刷新页面
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

关键点

  • 骨架屏:用占位内容代替白屏,减少用户等待焦虑
  • 防抖节流:控制高频事件触发频率,提升性能
  • 懒加载:延迟加载非关键资源,加快首屏速度
  • 乐观更新:先更新 UI 再请求,让操作感觉更快
  • 虚拟列表:只渲染可见内容,解决长列表性能问题
  • 状态反馈:加载、成功、失败都要有明确的视觉反馈