共计 3977 个字符,预计需要花费 10 分钟才能阅读完成。
背景
在之前的一篇对于有限滚动优化的文章中,咱们应用了虚构列表来改善用户体验,并获得了不错的成果。
本篇是后续,在虚构列表中的图片缩略图减少离屏渲染和压缩缓存的能力,作为性能加强。
次要的解决:
减少一个用离屏渲染压缩图片的 Avatar 组件,并替换原有的 Avatar 组建
;减少了 LRU Cache 来缓存压缩过后的图片
;实验性的退出 Web worker 避免压缩图片时主线程卡顿
;
下文次要是分享具体方法的实现,心愿对大家有所帮忙。
注释
咱们晓得,在虚构列表中,视区之外的节点是不会被渲染的,如图所示:
只有进入到视区,或者设定的某个阈值,比方高低一屏之内,才会挂载和渲染,以此来放弃总的结点数在一个正当的范畴内,以此较少浏览器的累赘,缩短屏幕响应工夫。
本地优化是针对列表中的图片,做了一些解决,来进步解决效率。
具体实现
页面接入
// 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 />
组件实现
// 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);
对应封装的 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 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,
});
}
}
});
相干配置批改
// 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 不熟的同学能够参考这篇文章。
内容就这么多,心愿对大家有所启发。
满腹经纶,如果谬误,欢送斧正,谢谢。
正文完