Серия Millions of PV Mall Practice — Практика по оптимизации ресурсов изображений переднего плана

JavaScript оптимизация производительности React.js
Серия Millions of PV Mall Practice — Практика по оптимизации ресурсов изображений переднего плана

⚠️ Эта статья является первой подписанной статьей сообщества Nuggets, и ее перепечатка без разрешения запрещена.

предисловие

百万PV商城系列В основном включает мой в проект торгового центра实践心得, я начну с视觉体验,性能优化,架构设计Начиная с трех измерений, я шаг за шагом объясню внешний вид проекта торгового центра.出现的问题,问题解决的思路与方案, и наконец代码实践. Это сделает вас более удобным, когда вы столкнетесь с подобными проблемами на работе.

Эта статья является первой в серии практических рекомендаций по работе с торговыми центрами. Основное ее содержание — оптимизация загрузки ресурсов изображений в распространенных сценариях в проектах торговых центров для улучшения визуального восприятия и производительности страниц.

задний план

в повседневной жизни,Yoyo们Должно быть, использовали некоторые компании электронной коммерцииApp,Web,小程序и другие приложения. В процессе просмотра будет много изображений, таких как общие карточки продуктов, сведения о продукте, карусельные изображения, рекламные изображения и другие компоненты, которые необходимо использовать для некоторых загруженных ресурсов изображения конфигурации управления фоном.

Кроме того, большинство страниц, на которых расположены эти компоненты, — это страницы с очень большой нагрузкой по трафику, и если пользовательский опыт и смысл относительно невелики, это неизбежно скажется на удержании и конверсии пользователя. Итак, в этой статье мы изучим некоторые навыки решения задач по оптимизации ресурсов изображения.Я по очереди представлю фактические операции в работе из трех сценариев: обычных изображений, изображений высокой четкости и рендеринга на большом экране. Если вы столкнулись с соответствующим сценарием оптимизации, вы можете не волноваться.

image.png

Обычная оптимизация изображения

Для общей оптимизации изображения я использую ленивую загрузку в качестве основного метода реализации. Проще говоря, ленивая загрузка означает, что ресурсы изображения в визуальном окне загружаются первыми, а содержимое за пределами визуального окна будет загружаться только после прокрутки в визуальное окно.

Итак, почему я использую ленивую загрузку и какова реализация ленивой загрузки? Ниже я передаю простойReactРеализация схемы ленивой загрузки изображения, чтобы объяснить шаг за шагом.

Зачем использовать ленивую загрузку?

Вообще говоря, большое количество ресурсов изображений будет загружено, как только вы войдете на веб-страницу, что косвенно повлияет на загрузку страницы, увеличит время загрузки белого экрана и повлияет на работу пользователя. Поэтому наше обращение不在可视化窗口Изображения внутри не загружаются счастливо, насколько это возможно减少本地带宽отходы и请求资源количество.

Итак, почему я рекомендую ленивую загрузку в качестве основного метода реализации общих ресурсов изображений? Потому что у него есть два преимущества.

  • Уменьшите потребление ресурсов полосы пропускания и уменьшите ненужное потребление ресурсов при загрузке.
  • Предотвратите блокировку загрузки ресурсов, вызванную одновременной загрузкой ресурсов изображения, тем самым сократив время белого экрана.

image.png

Реализовать простую ленивую загрузку

Итак, как реализовать ленивую загрузку? Есть два способа добиться этого.

  • пройти черезscrollСобытия для прослушивания реализации области прокрутки окна. Этот метод имеет хорошую совместимость, и большинство браузеров иWebViewсовместимы и поддерживаются.
  • пройти черезIntersectionObserver APIнаблюдатьDOMНезависимо от того, появляется ли он в окне или нет, преимущество этого метода в том, что его просто вызвать, но совместимость с некоторыми мобильными терминалами не так хороша, как у предыдущего метода.

Обе формы наблюдают за текущимDOMПоявляется ли оно в видимом окне, если да, то будетdata-srcАдрес изображения в назначенsrc, затем начинает загрузку текущего изображения.

Итак, приступим к реализацииscrollПример ленивой загрузки событий.

Макет страницы

Сначала мы рисуем базовый макет страницы, в основном для загрузки окон и изображений.

const ImageLazy = () => {
  
  const [list, setList] = useState([
    1,2,3,4,5,6,7,8
  ])

  const ref = useRef<HTMLDivElement | null>(null)

  return (
    <div className="scroll-view" ref={ ref }>
      {list.map((id) => {
          return (
            <div key={id} className="scroll-item">
              <img
                style={{ width: '100%', height: '100%' }}
                data-src={ `${ prefix }split-${id}.jpg` }
              />
            </div>
          );
        })}
    </div>
  )
}
.scroll-item {
  height: 200px;
}

.scroll-view {
  height: 400px;
  overflow: auto;
}

Вы можете видеть рендеры, на странице отображаются только две картинки, но на самом деле все картинки загружены.

image.png

Зарегистрируйтесь на события прокрутки

заscroll-viewграницаrefПосле этого также необходимоuseEffectсредняя параscrollСобытия связаны и незарегистрированы.

Таким образом, я сначала получаю все текущие компонентыimgЭлемент (фактическая операция лучше всего использовать указанное className), которыйref.currentпровестиaddEventListenerДобавьте операцию прослушивателя событий, а затем выполните соответствующий метод в обратном вызове.

В то же время вreturnКогда событие происходит, также необходимо удалить его событие, чтобы избежать некоторых непредвиденных ситуаций.

useEffect(() => {
  const imgs = document.getElementsByTagName('img');
  console.log(ref.current, 'current')
  ref.current?.addEventListener('scroll', () => {
    console.log('listens run')
  })
  return (
    ref.current?.removeEventListener('scroll', () => {
      console.log('listens end')
    })
  )
}, [])

Как показано ниже, когда я прокручиваю, он выполняется одновременноScrollCallback, консоль выводит много результатов выполнения, а это значит, что наше событие успешно добавлено.

image.png

Обратный вызов прокрутки и функция дроссельной заслонки

Из приведенной ниже диаграммы анализа мы видим, чтоclientHeight высота нашего окна, а вScrollViewкоторый срабатывает каждый раз, когда вы прокручиваетеscrollОбратный вызов метода, вы можете получить расстояние прокрутки окна текущей страницыscrollTop .

Итак, как мы можем узнать, присутствует ли элемент на странице?

по элементуoffsetTopсвойство, вы можете узнать расстояние смещения текущего элемента сверху. Итак, когда мы получаем высоту окнаclientHeight, расстояние прокаткиscrollTop, и расстояние элемента от вершиныoffsetTop, можно вывести следующий набор условных формул,视窗高度(dom.clientHeight) + 滚动距离(dom.scrollTop) > 元素距离顶部距离(image.offsetTop)Чтобы определить, появляется ли текущий элемент в видимом диапазоне страницы.

image.png

Преобразование его в функциональный метод дает следующие результаты:

function scrollViewEvent (images: HTMLCollectionOf<HTMLImageElement>) {
    
  // 可视化区域高度
  const clientHeight = ref.current?.clientHeight || 0
  
  // 滚动的距离
  const scrollTop = ref.current?.scrollTop || 0
  
  // 遍历imgs元素
  for (let image of images) {
    if (!image.dataset.src) continue
  
    // 判断src是否已经加载
    if (image.src) continue
    
    //图片距离顶部距离
    let top = image.offsetTop
    
    // 公式
    if (clientHeight + scrollTop > top) {
     // 设置图片源地址,完成目标图片加载
      image.src = image.dataset.src || ''
      image.removeAttribute('data-src')
    }
  }
}

Вот я тоже прохожуahookсерединаuseThrottleFnВыполните небольшую оптимизацию регулирования, чтобы избежать частых обратных вызовов функций. существует500msВнутри событие будет выполнено только один раз, чтобы избежать потребления производительности, вызванного дополнительным выполнением.

import { useThrottleFn } from 'ahooks'

// 截流函数hook
const { run } = useThrottleFn(scrollViewEvent, {
  wait: 500
})

При этом мы вставляем егоscrollВыполняется в обратном вызове события. Однако в начале компонента он не может быть активированscrollсобытие, поэтому нам нужно вручную инициализировать данные изображения на текущей первой странице.

useEffect(() => {
  const imgs = document.getElementsByTagName('img');
  console.log(ref.current, 'current')
  ref.current?.addEventListener('scroll', () => {
    run(imgs)
  })
  run(imgs)
  return () => {
    ref.current?.removeEventListener('scroll', () => {
      console.log('listens end')
    })
  }
}, [])

Пока что один из наших图片懒加载Реализация в основном завершена, давайте посмотрим на эффект.

image.png

Для ленивой загрузки каждыйitemЛучше всего установить высоту, чтобы ленивая загрузка элемента страницы не отображалась в области просмотра из-за отсутствия высоты компонента, когда в начале нет изображения.

Высокоточная оптимизация изображения

Для высококачественных изображений многие создаются相关运营人员Настроенные диаграммы активности, как правило их будет много в период акции微页面,или图片链接, на всем протяжении图片 + 热区публикуется для просмотра пользователями.

Поэтому подавляющее большинство эксплуатационных требований заключается в максимально четком отображении соответствующих изображений. Тогда ленивая загрузка, очевидно, не является хорошим решением проблемы, поэтому я добавил несколько состояний загрузки, основанных на исходной ленивой загрузке, чтобы дать пользователям визуальный опыт.В настоящее время продукты на рынке в основном используют骨架屏или渐进式加载И другие решения, чтобы сделать переход отображения изображения более плавным, чтобы избежать смущения из-за сбоя загрузки или зависания загрузки изображений.

Как добиться

На следующем рисунке давайте сначала разберемся с логикой реализации. В компоненте изображения поочередно заменяются два изображения.Когда загружается изображение ресурса высокой четкости, его необходимо骨架图или缩略图Скрыть, показать загруженное изображение высокой четкости.

image.png

компонент изображения

После завершения анализа вы можете следовать за мной, чтобы реализовать простой компонент изображения черезimgсерединаonLoadсобытие, чтобы определить, было ли загружено отображаемое изображение. через соответствующий状态(status)для управления отображением и скрытием эскизов. Далее я буду использовать прогрессивную загрузку в качестве примера.Ссылаясь на рисунок ниже, мы реализуем простой компонент переключения состояний.

import React from "react";
import "./index.css";

interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
  thumb: string;
}

type ImageStatus = 'pending' | 'success' | 'error'

const Image: React.FC<ImageProps> = (props) => {
  const [status, setImageStatus] = React.useState<ImageStatus>('pending');

  /**
   * 修改图片状态
   * @param status 修改状态
   */
  const onChangeImageStatus = (status: ImageStatus) => {

    /** TODO setTime模拟请求时间 */
    setTimeout(() => setImageStatus(status), 2000)
  }

  return (
    <>
      <img
        className="image image__thumb"
        alt={props.alt}
        src={props.thumb}
        style={{ visibility: status === 'success' ? "hidden" : "visible" }}
      />
      <img
        onLoad={() => onChangeImageStatus('success')}
        onError={() =>onChangeImageStatus('error')}
        className="image image__source"
        alt={props.alt}
        src={props.src}
      />
    </>
  );
};

export default Image;

пройти черезfilter: blur(25px);Собственность, часть миниатюры, чтобы добавить размытие, поэтому вы можете избежать смущения некоторых из карт мозаики, чтобы достичь части земного стекла или гауссовского размытия некоторых небольших эффектов.

iShot2021-07-20 00.58.24.gif

пройти черезonLoadПосле события загрузки мы выполнили простую прогрессивную загрузку изображения, поэтому другие состояния загрузки, подобные скелетному экрану, также могут быть реализованы с помощью того же суждения о состоянии. Просто поменяйте миниатюры на другие компоненты и все.

Оптимизирован для длинных рендеров

На странице сведений о продукте операция настроит подробное описание изображений и текстов некоторых продуктов, не только качество изображений будет относительно высоким, но и изображения будут очень длинными. Так что, очевидно, мы не можем сказать, что можем напрямую получить картинку и отобразить ее на странице, если скорость интернета у пользователя относительно низкая, то прямо на странице появится длинная белая полоса или карта ошибок, которая не загружается. . . . Это явно не те результаты, которые нам нужны.

Что же тогда делать?

Давайте сначала посмотрим на детали продукта на платформах электронной коммерции, таких как Taobao.Когда вы нажмете на большую картинку, вы обнаружите, что отображается только часть изображения, и оно будет разделено на множество изображений одинакового размера для нас.

В соответствии с этой идеей мы также выполнили соответствующую оптимизацию сегментации изображения, разделив длинное изображение на несколько блоков одинакового размера, чтобы выполнить оптимизацию пакетного рендеринга, чтобы уменьшить нагрузку при одиночном рендеринге длинных изображений.

image.png

разрезать на кусочки

Затем я вырезаю изображение из длинного изображения смоделированного бэкэнда и по очереди отображаю вырезанные изображения на странице для отображения. Без лишних слов, давайте сразу к делу.

Прежде всего, в сочетании со следующей диаграммой анализа, наш принцип разрезания диаграммы на самом деле очень прост.Разделите длинное изображение на маленькие изображения с одинаковой длиной и шириной.Если последнее изображение не удовлетворяет切割块高度Если это так, непосредственно обрежьте оставшуюся высоту на изображениях.

image.png

Итак, позвольте мне написать простой код узла, чтобы показать вам процесс разрезания графа.

Срез узла

Как показано на рисунке ниже, я смоделирую длинную картинку, загруженную операцией, а затем разрежу ее на несколько частей нужной высоты.200Короткое изображение (размер по оценке потребностей). Теперь давайте взглянем на учебник и код для достижения эффекта.

image.png

Сначала установитеsharpдля обработки изображений,image-sizeИспользуется для получения информации о размере изображения.

# shell

yarn add sharp image-size

После импорта соответствующего пакета перейдитеimage-sizeПолучите информацию об исходном длинном изображении, которое нам нужно вырезать, напримерwidthиheight.

const sharp = require('sharp')

const sizeOf = require('image-size');

const currentImageInfo = sizeOf('./input.jpg');

При получении высоты и ширины изображения это означает, что я могу передатьwhileЦикл вычисляет высоту отрезанных аликвот.

/** @name 每块大小 200px height */
const SPLIT_HEIGHT = 200

/** @name 长图高度 */
let clientHeight = currentImageInfo.height

/** @name 切割小图高度 */
const heights = []

while (clientHeight > 0) {
  /** @if 切图高度充足时 */
  if (clientHeight >= SPLIT_HEIGHT) {
    heights.push(SPLIT_HEIGHT)
    
    clientHeight -= SPLIT_HEIGHT
    
  } else {
    /** @else 切割高度不够时,直接切成一张,高度清0 */
    heights.push(clientHeight)
    
    clientHeight = 0
    
  }
}

image.png

Затем, когда я знаю размер вырезанного изображения, я могуheightsпройти через裁剪偏移Нарезайте настоящие изображения, создавайте новые файлы и сохраняйте их.

В приведенном ниже коде я создаюmarginTopсмещение, каждый раз, когда он разрезается, он будетheightНакапливайте смещение вниз до тех пор, пока вырезанное изображение не закончится в конце последней страницы. В настоящее времяmariginTopэто высота изображения.

/** @name 偏移量 */
let marginTop = 0

heights.forEach((h, index) => {
  sharp('./input.jpg')
    .extract({ left: 0, top: marginTop, width: currentImageInfo.width, height: h })
    .toFile(`./img/split_${index + 1}_block.jpg`).then(info => {
      console.log(`split_${index + 1}_block.jpg切割成功`)
    }).catch(err => {
      console.log(JSON.stringify(err), 'error')
    })
    marginTop += h
})

Как показано ниже,img文件夹немного больше零碎的图片, затем проверьте, является ли изображение拼接完整, если нет проблем, то мы выполнили требование простого длинного дайсинга карты, и следующим шагом будет поставить его на передний конец渲染.

image.png

Фронтальный дисплей

После того, как фронтенд получает соответствующие срезы, они напрямую собираются и отображаются на странице фронтенда, после удаления пробелов или заусенцев в середине ничем не отличается от рендеринга длинных изображений. При рендеринге я все еще делаю простую оптимизацию загрузки в виде ленивой загрузки, общий эффект такой:

image.png

Прочитав эффект загрузки, давайте посмотрим на время отклика загрузки.

Так как я вырезал картинку и не сделал文件上的优化, поэтому единственный граф существует体积过大, но это никак не влияет на загрузку первого экрана Вы можете увидеть картинку ниже.Flow3GСравнение времени загрузки ниже.

Для одного длинного изображения пользователь может увидеть около 4 секунд.白屏или骨架屏, а загрузка после нарезки может предпочтительно отображать часть содержимого для пользователя.

image.png

Устранение заусенцев или зазоров

В окончательной реализации этого решения могут быть некоторые дефекты, а конкретный план раскроя связан с моделью оборудования. Если у вас возникнут проблемы, вы можете обратиться к некоторым из моих решений:

  • Первый черезvertical-centerУстановите значение вертикального центра, чтобы решить проблемы с выравниванием базовой линии.
  • Второй -imgустановить на реальныйblockЭлементы решены.
  • Третий, который я обычно использую, черезflex-directionУстановить какcolumnВертикальное расположение дочерних элементов решает проблему.
  • Четвертый черезbackgroundрешение задач с картинками. Но при этом, если вы хотите использовать ленивую загрузку, вам нужно изменить частьcssСмещение стиля для достижения видимого отображения окна.

Справочные ресурсы

Суммировать

В этой статье я рассказал о стратегиях оптимизации для трех разных изображений. Их уже много на рынке开源库более удобный в реализации懒加载и渐进加载Путь. В то же время для каркасного экрана во многих библиотеках компонентов есть соответствующие компоненты, и стоимость упаковки также невелика.

Поэтому, если в проекте действительно задействовано много ресурсов изображений, то схема оптимизации, упомянутая в статье, — это то, что я рекомендую.

  • Если его можно сделать ленивым, постарайтесь не загружать его полностью
  • Дайте пользователю определенную подсказку о статусе и постарайтесь не открывать каркасный экран или диаграмму перехода.
  • Длинный граф можно максимально обрезать, и его очень удобно разбирать для оптимизации.
  • Изображения, которые можно сжать, должны быть максимально сжаты.

Для проекта торгового центра задача заключается не в логике реализации функций, а в оптимизации части визуального опыта и впечатлений. Если вы считаете, что статья полезна для вас, вы можете нажать 👍 и подбодрить меня. Если вы хотите узнать больше о внешних проектах электронной коммерции, Yoyos может подписаться на эту колонку.

последние хорошие статьи

концевые сноски

Эта статья была впервые опубликована в: Nuggets Technology Community
Тип: Подписанная статья
Добавить Автора
Любимое в колонке:# Миллионы серии PV Mall Practice
Публичный номер: Жизнь программы ItCodes