6

image.png

background

Currently, when the user selects 200+ sku and edits it, the page has performance problems, the response is slow, and the user cannot operate normally.

At present, we all use the original image uniformly. The original image may have 1024 * 1024, but the actual display may be very small, which inevitably leads to waste of some traffic. Such a large traffic also causes our pages to load not fast enough, especially When there are some long lists, because of the compression of the image size, our page has some lags.

In a previous article on infinite scroll optimization , we used dummy lists to improve the user experience with good results.

The general principle of the virtual list is: only after entering the viewport, or a certain threshold set, such as the upper and lower screens, will it be mounted and rendered, so as to keep the total number of nodes within a reasonable range, This reduces the burden on the browser and shortens the screen response time.

This article is a follow-up. The ability of 离屏渲染 and 压缩并缓存 is added to the image thumbnails in the virtual list as a function enhancement.

The purpose of optimization:

  • Support 2000 sku+ can operate normally at the same time;
  • The loading time of entering the page is within 2-3s, the scrolling display is not stuck, and the operation feedback is normal;
  • Faster page loading speed;

Main processing:

  1. 增加一个用离屏渲染压缩图片的 Avatar 组件, 并替换原有的 Avatar 组建 ;
  2. 增加了 LRU Cache 来缓存压缩过后的图片 ;
  3. 实验性的加入 Web worker 防止压缩图片时主线程卡顿 ;
  4. Use the more powerful react-virtualized instead of the original react-virtual-list

The following mainly shares the implementation of 方案设计 and 核心代码 , I hope it will be helpful to everyone.

text

Situation Analysis

At present, we all use the original image uniformly. The original image may be larger than 1024 * 1024 , but the actual display may be very small, and the corresponding size of the image is not cropped.

Waste user loading traffic, and in some virtual scrolling, when it comes to DOM creation and deletion, 交互响应有明显的卡顿 .

1.gif

Existing Architecture

Upload product pictures on the front end:

image.png

image.png

Weaknesses of Existing Architecture

Using the wrong image size will inevitably lead to a waste of some traffic. Such a large amount of traffic also causes our pages to load not fast enough, especially when there are some long lists, because of the compression of the image size, our pages have some freezes. .

Stuttering and slow loading speed will affect the user experience and reduce the experience quality of the product.

improve proposals

Upload and get pictures

image.png

image.png

Front-end fetching pictures and fallback logic

image.png

Get the image of the target size according to the parameters to save traffic.

Front-end image compression

flow chart

image.png

Comparison of before and after optimization

Before optimization

After selecting all SKUs, the page will pop up a crash prompt after waiting for 10s.

1.gif

image.png

Optimized

Except for the first loading of data, there is a loading wait, and subsequent scrolling and interactions do not have any lag.

2.gif

image.png

Data comparison

image.png

image.png

The core rendering stuck problem has been solved, the function has changed from available to unavailable, and the user experience has been greatly improved.

Legacy issues and risks

leave questions

  1. It only involves scenes that are frequently used by users, and does not cover the full amount of pictures;
  2. Inability to reduce user traffic;
  3. Increase the client running memory by a small amount.

Risk estimation

  1. Pictures below the threshold will not be processed;
  2. There may be omissions in the update of historical data;
  3. For some low-version users, it may not be available and fall back to the old logic.

Detailed implementation of the core code

page access

 // view
import AvatarWithZip from '../molecules/AvatarWithZip';

- <Avatar className="product-image" src={record.image} size={32} />
+ <AvatarWithZip className="product-image" src={record.image} size={32} openZip />

Detailed implementation of key components

Component implementation
 // AvatarWithZip
import React, { useState } from 'react';
import Avatar, { AvatarProps } from 'antd/lib/avatar';
import useAvatarZipper from '@/hooks/use-avatar-zipper';

interface PropTypes extends AvatarProps{
  openZip: boolean;
}

const AvatarWithZip: React.FC<PropTypes> = (props) => {
  const { openZip, src, size, ...otherProps } = props;
  const [localSrc, setLocalSrc] = useState(openZip ? '' : src);
  const resize = useAvatarZipper(size as number);
  React.useEffect(() => {
    if (!openZip) {
      return;
    }
    if (typeof src === 'string') {
      resize(src).then(url => {
        setLocalSrc(url);
      });
    } else if (typeof src === 'object') {
      setLocalSrc(src);
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [src, openZip]);

  return (
    <Avatar src={localSrc} size={size} {...otherProps} />
  );
};

export default React.memo(AvatarWithZip);

Corresponding package hooks

 import React from 'react';
import LRU from 'lru-cache';
import AvatarZipWorker from '@/workers/avatar-zip.worker.ts';

type UseAvatarZipper = (src: string) => Promise<string>;

const MAX_IAMGE_SIZE = 1024;

const zippedCache = new LRU<string, string>({
  max: 500,
});

const worker = new AvatarZipWorker();

const useAvatarZipper: (size?: number) => UseAvatarZipper = (size = 32) => {
  const offScreen = React.useRef<OffscreenCanvas>(new OffscreenCanvas(size, size));
  const offScreenCtx = React.useRef(offScreen.current.getContext('2d'));

  React.useEffect(() => {
    offScreen.current = new OffscreenCanvas(size, size);
    offScreenCtx.current = offScreen.current.getContext('2d');
    worker.postMessage({ type: 'init', payload: size });
  }, [size]);

  const resize: (src: string, max?: number) => Promise<string> = React.useCallback((src, max = MAX_IAMGE_SIZE) =>
    new Promise<string>((resolve, reject) => {
      const messageHandler = (event: MessageEvent) => {
        const { type, payload } = event.data;
        if (type === 'error') {
          reject(payload);
        }
        const { origin, dist } = payload;
        if (origin === src) {
          zippedCache.set(src, dist);
          resolve(dist);
        }
      };
      if (zippedCache.has(src)) {
        resolve(zippedCache.get(src) as string);
        return () => {};
      }
      worker.postMessage({ type: 'zip', payload: { src, max } });
      worker.addEventListener('message', messageHandler);
      return () => {
        worker.removeEventListener('message', messageHandler);
      };
    }),
  []);

  return resize;
};

export default useAvatarZipper;

worker implementation

 const ctx: Worker = self as any;

interface MessageData {
  type: string;
  payload: any;
}

export interface MessageReturnData {
  type: string;
  payload: any;
}

let offScreen: OffscreenCanvas;
let offScreenCtx: OffscreenCanvasRenderingContext2D | null;

// Respond to message from parent thread
ctx.addEventListener('message', async (event: MessageEvent<MessageData>) => {
  const { data } = event;
  const { type, payload } = data;
  
  if (type === 'init') {
    offScreen = new OffscreenCanvas(payload, payload);
    offScreenCtx = offScreen.getContext('2d');
  }
  
  if (type === 'zip') {
    const { src, max } = payload;
    try {
      if (!offScreenCtx) {
        throw Error();
      }
      const res = await fetch(src);
      const srcBlob = await res.blob();
      const imageBitmap = await createImageBitmap(srcBlob);

      if (Math.max(imageBitmap.width, imageBitmap.height) <= max) {
        ctx.postMessage({
          origin: src,
          dist: src,
        });
      }

      const size = offScreen.width;
      offScreenCtx.clearRect(0, 0, size, size);
      offScreenCtx.drawImage(imageBitmap, 0, 0, size, size);
      
      const blobUrl = await offScreen.convertToBlob().then(blob => URL.createObjectURL(blob));
      ctx.postMessage({
        type: 'success',
        payload: {
          origin: src,
          dist: blobUrl,
        }
      });
    } catch (err) {
      ctx.postMessage({
        type: 'error',
        payload: err,
      });
    }
  }
});

Related configuration modification

 // webpack 增加配置:
  {
    test: /\.worker\.ts$/,
    loader: 'worker-loader',
    options: {
      chunkFilename: '[id].[contenthash].worker.js',
    },
  }

  // types 

  declare module '*.worker.ts' {
    class WebpackWorker extends Worker {
      constructor();
    }

    export default WebpackWorker;
  }

Summarize

In general, the implementation idea is not complicated, mainly the implementation of workers, and the processing of boundary problems, such as downgrade processing.

Students who are not familiar with workers can refer to this article .

That's all there is to it, I hope it inspires you all.

If I am wrong, please correct me, thank you.


皮小蛋
8k 声望12.8k 粉丝

积跬步,至千里。