背景

在之前的一篇对于有限滚动优化的文章中, 咱们应用了虚构列表来改善用户体验,并获得了不错的成果。

本篇是后续,在虚构列表中的图片缩略图减少离屏渲染和压缩缓存的能力, 作为性能加强。

次要的解决:

  1. 减少一个用离屏渲染压缩图片的 Avatar 组件, 并替换原有的 Avatar 组建
  2. 减少了 LRU Cache 来缓存压缩过后的图片
  3. 实验性的退出 Web worker 避免压缩图片时主线程卡顿

下文次要是分享具体方法的实现, 心愿对大家有所帮忙。

注释

咱们晓得, 在虚构列表中, 视区之外的节点是不会被渲染的, 如图所示:

只有进入到视区, 或者设定的某个阈值, 比方高低一屏之内, 才会挂载和渲染, 以此来放弃总的结点数在一个正当的范畴内, 以此较少浏览器的累赘, 缩短屏幕响应工夫。

本地优化是针对列表中的图片, 做了一些解决, 来进步解决效率。

具体实现

页面接入

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

组件实现

// AvatarWithZipimport 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);

对应封装的 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 实现

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 threadctx.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,      });    }  }});

相干配置批改

  // 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;  }

总结

总的来说, 实现思路并不简单, 次要是 worker 的实现,以及边界异样解决, 比方降级解决等。

对 worker 不熟的同学能够参考这篇文章。

内容就这么多, 心愿对大家有所启发。

满腹经纶,如果谬误, 欢送斧正, 谢谢。