背景
在之前的一篇对于有限滚动优化的文章中, 咱们应用了虚构列表来改善用户体验,并获得了不错的成果。
本篇是后续,在虚构列表中的图片缩略图减少离屏渲染和压缩缓存的能力, 作为性能加强。
次要的解决:
减少一个用离屏渲染压缩图片的 Avatar 组件, 并替换原有的 Avatar 组建
;减少了 LRU Cache 来缓存压缩过后的图片
;实验性的退出 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 不熟的同学能够参考这篇文章。
内容就这么多, 心愿对大家有所启发。
满腹经纶,如果谬误, 欢送斧正, 谢谢。