首页/文章/javascript

你的瀑布流还不够懒

2024-09-03
22708 分钟
...

无限加载的商品瀑布流是电商最场景的最常用、最重要的组件,因此有一个高性能的瀑布流组件就变得无比重要

使用 Grid 布局,赢在起跑线

Grid 布局实现响应式非常简单,纯 CSS 实现,不依赖 JavaScript,性能优化赢在起点上

<style>
  .container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 12px;
    padding: 12px;
  }
</style>

<div class="container">
  <div class="card">
    <img src="https://via.placeholder.com/300" alt="商品1">
    <h2>商品名称 1</h2>
    <p>商品描述 1</p>
  </div>
  <div class="card">
    <img src="https://via.placeholder.com/300" alt="商品...">
    <h2>商品名称 ...</h2>
    <p>商品描述 ...</p>
  </div>
  
  <div class="card">
    <img src="https://via.placeholder.com/300" alt="商品6">
    <h2>商品名称 6</h2>
    <p>商品描述 6</p>
  </div>
</div>

使用 IntersectionObserver 实现懒加载

传统的懒加载通过监听 scroll 事件判断目标元素是否接近/出现在可视区,发起异步请求、加载更多的卡片。而现代浏览器可以使用浏览器原生的 IntersectionObserver API 监测目标元素是否出现在 Viewport

使用 IntersectionObserver 可以异步执行回调函数,无需频繁监听滚动事件,这样可以显著降低 CPU 和内存使用,从而提升性能

const target = document.getElementById('targetElement');


const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      
      target.style.backgroundColor = 'lightgreen'; 
      console.log('元素已进入 Viewport!');
    } else {
      
      target.style.backgroundColor = 'lightblue'; 
      console.log('元素已离开 Viewport!');
    }
  });
});


observer.observe(target);

因此我们瀑布流的代码可以稍加改进

import React, { useEffect, useRef, useState } from 'react';

const Waterfall = () => {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);
  const observerRef = useRef();

  const fetchItems = async () => {
    setLoading(true);
    
    const newItems = Array.from({ length: 10 }, (_, index) => ({
      src: 'https://via.placeholder.com/300x300',
    }));

    setTimeout(() => {
      setItems(prevItems => [...prevItems, ...newItems]);
      setLoading(false);
    }, 1000); 
  };

  useEffect(() => {
    const loadMore = (entries) => {
      if (entries[0].isIntersecting) {
        fetchItems();
        observerRef.current.disconnect(); 
      }
    };

    const observer = new IntersectionObserver(loadMore, {
      rootMargin: '1000px', 
    });

    const target = document.querySelector('#load-more');
    if (target) {
      observer.observe(target);
    }

    return () => {
      observer.disconnect(); 
    };
  }, [items]);

  
  useEffect(() => {
    fetchItems();
  }, []);

  return (
    <div className="waterfall-container">
      {items.map(item => (
        <div className="item" key={item.id}>
          <img src={item.src} alt={`Loaded item ${item.id}`} />
        </div>
      ))}
      {loading && <div>加载中...</div>}
      <div id="load-more" style={{ height: '20px', marginBottom: '20px' }} />
    </div>
  );
};

export default Waterfall;

原生的图片懒加载 loading="lazy"

我们虽然已经通过 IntersectionObserver 对组件做了初步的懒加载,但还可以更进一步对 Viewport 的图片也做懒加载,传统也是通过监听 scroll 事件实现,现在大部分主流浏览器通过load="lazy"原生支持了图片懒加载,简单又高效

<img src="image-to-lazy-load.jpg" loading="lazy">

当对图片设置了这个属性后,浏览器会根据自己的启发式算法决定图片的加载时机。这些算法会考虑多个因素,比如图片即将进入视口的距离,或者用户当前的网络条件等。通常启发式算法的工作方式如下:

  • 视口接近度:浏览器会监测页面滚动,检查懒加载图片距离视口的距离。当图片快要出现在视口内时,浏览器会开始加载图片。具体开始加载图片的距离阈值并没有统一的标准,不同的浏览器可能会有不同的实现。
  • 网络状况:一些浏览器可能会根据用户的网络状况(例如是否使用数据流量或者Wi-Fi)来决定是否提前加载图片。
  • CPU和内存使用情况:如果用户设备的CPU或内存使用率很高,浏览器可能会延迟加载图片,直到资源使用减少。
  • 电池状态:对于移动设备,浏览器可能会在电池电量充足时更积极地加载资源。

虽然开发者无法精准控制图片加载的时机,但浏览器原生支持考虑的因素不仅仅是滚动位置,相对而言更加合理。顺便说一句,使用 JavaScript 懒加载本身也有性能开销,可能会影响到页面的 FPS

非首屏图片异步解码

解码图像和视频是计算密集型的操作,可能会占用大量的CPU资源,特别是对于高分辨率或者复杂编码格式的媒体文件,如果主线程被图像或视频的解码操作阻塞,用户在滚动页面或尝试交互时可能会感受到卡顿或延迟
对非首屏图片或视频添加 decoding="async" 可以允许浏览器在后台处理图片、视频解码,而不阻塞主线程,继续处理和渲染页面的其余部分,这样可以有助于改善页面的加载性能,减少用户感知到的延迟,并提供更加平滑的用户体验

<img src="image.jpg" decoding="async">

使用 useTransition 保证滚动丝滑

当用户滚动出发瀑布流不断加载时候 React 需要反复渲染商品卡片,这样的长时间计算可能会用户感到浏览器延迟或卡顿

React 18 引入了并发模式(Concurrent Mode)让开发者可以将某些状态更新标记为可中断的,从而允许 React 在必要时推迟这些更新,优先处理其它更为紧急的任务,使用useTransition 即可让开发者可以非阻塞的方式渲染 UI

import React, { useState, useTransition } from 'react';

const ExampleComponent = () => {
  const [items, setItems] = useState([]);
  const [isPending, startTransition] = useTransition();

  const addItem = () => {
    startTransition(() => {
      
      setItems(prevItems => [...prevItems, `Item ${prevItems.length + 1}`]);
    });
  };

  return (
    <div>
      <button onClick={addItem} disabled={isPending}>
        {isPending ? 'Adding...' : 'Add Item'}
      </button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

由于瀑布流滚动加载的商品属于预加载,我们可以利用 useTransition 在必要时候推迟 React 渲染,防止用户浏览器卡顿


import React, { useEffect, useRef, useState, useTransition } from 'react';
import './App.css'; 


const fetchItems = (count) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(
        Array.from({ length: count }, (_, index) => {
          const height = Math.floor(Math.random() * (300 - 100 + 1)) + 100; 
          return {
            height,
            src: `https://via.placeholder.com/200x${height}`,
          };
        })
      );
    }, 1000);
  });
};

const Waterfall = () => {
  const [items, setItems] = useState([]);
  const [isPending, startTransition] = useTransition();
  const observerRef = useRef();

  const loadItems = () => {
    startTransition(() => {
      fetchItems(10).then((newItems) => {
        setItems((prev) => [...prev, ...newItems]); 
      });
    });
  };

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          loadItems();
          observer.disconnect();
        }
      },
      { rootMargin: '1000px' } 
    );

    if (observerRef.current) {
      observer.observe(observerRef.current);
    }

    return () => {
      observer.disconnect(); 
    };
  }, [items]);

  
  useEffect(() => {
    loadItems();
  }, []);

  return (
    <div className="waterfall-container">
      {items.map((item, index) => (
        <div className="item" key={index}>
          <img
            src={item.src}
            loading="lazy"
            decoding="async"
            alt={`Loaded item ${index + 1}`}
            style={{ height: item.height + 'px' }}
          />
          <h2>商品名称 {index + 1}</h2>
          <p>商品描述 {index + 1}</p>
        </div>
      ))}
      {isPending && <div>加载中...</div>}
      <div id="load-more" ref={observerRef} />
    </div>
  );
};

export default Waterfall;

延迟浏览器渲染 Viewport 之外元素

content-visibility 是 CSS 属性,允许浏览器跳过不在屏幕上的元素的渲染工作,直到用户滚动到它们的位置。通过跳过不可见内容的渲染,content-visibility 可以显著减少页面的初始加载时间,并降低内存的使用,从而改善用户体验。配合 contain-intrinsic-size 属性可以对容器进行渲染前的占位

<style>
  .image-gallery {
    content-visibility: auto;
    contain-intrinsic-size: 340px 340px; 
  }
</style>

<div class="image-gallery">
  <img src="image1.jpg" alt="描述1">
  <img src="image2.jpg" alt="描述2">
  
</div>

demo 中的 CSS 可以稍加改进

.waterfall-container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 10px;
  padding: 10px;
}

.item {
  content-visibility: auto;
  padding: 10px;
  background-color: #f2f3f7;
  border-radius: 5px;
}

.item img {
  width: 100%;
}

压缩率更高的图片格式 AVIF

大部分 Web 开发者对 WebP 格式非常熟悉了,但可能对 AVIF 格式还没有开始应用。AVIF 是一种基于 AV1 视频编码的新图像格式,用于将AV1压缩的图片或图片序列存储为HEIF文件格式。相对于JPEG,WEBP 这类图片格式来说,它的压缩率更高,并且画面细节更好,AVIF vs JPEG 大小节省约 50%,AVIF vs WebP 大小节省约 20%。主流浏览器的支持情况非常不错

浏览器在图片请求时候会在 Accept 头部信息中声明支持的图片格式,可以利用这个在 CDN 识别,使用相同的图片地址,返回不同格式的图片内容

避免前端加载 1px 透明图判断浏览器是否支持特定图片格式,然后修改图片 URL 来获取对应格式图片。这样的处理方式有两个弊端

  • 发起图片请求依赖前端格式判断的异步过程,请求时机被推迟
  • 使用新格式的图片包括后期的调整等,需要修改前端代码

小结

瀑布流可以使用以下手段进行性能优化

  • 使用纯 CSS 实现 Grid 布局优化响应式性能
  • 使用浏览器原生的 IntersectionObserver API 实现懒加载
  • 使用浏览器原生的 loading="lazy" 实现图片懒加载
  • 使用 decoding="async" 对图片进行异步解码
  • 使用 useTransition 非阻塞渲染商品卡片
  • 使用 content-visibility 延迟 Viewport 之外的元素渲染
  • 使用压缩率更高的图片格式 AVIF

这样的瀑布流才足够懒!

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录