背景目前用户选中200+个skus编辑时,页面出现性能问题,响应慢,用户无法正常操作。目前我们都是统一使用原图。原图可能有1024*1024,但实际显示可能很小,难免造成一些流量浪费。这么大的流量也导致我们的页面加载速度不够快,尤其是有一些长列表的时候,因为图片尺寸的压缩,我们的页面有点卡顿。在之前关于无限滚动优化的文章中,我们使用虚拟列表来改善用户体验并取得了很好的效果。虚拟列表的一般原则是:只有进入视口,或者设定的某个阈值,比如在上下屏之内,才会挂载渲染,让节点总数保持在一个合理的范围内range,这样就减轻了浏览器的负担,缩短了屏幕响应时间。这篇文章是后续。虚拟列表中的图像缩略图增加了渲染离屏、压缩和缓存的能力,作为功能增强。优化目的:支持2000个sku+同时正常运行;进入页面加载时间在2-3s以内,滚动显示不卡顿,操作反馈正常;页面加载速度更快;主要处理:添加离屏渲染压缩图像的Avatar组件,并替换原来的Avatar组件;添加LRUCache,对压缩后的图片进行缓存;实验性添加Webworker,防止压缩图片时主线程卡顿;使用更强大的react-virtualized代替原来的react-virtual-list下面主要分享方案设计和核心代码的实现,希望对大家有所帮助。文本状态分析目前我们都是统一使用原图。原图可能比1024*1024大,但实际显示可能很小,相应尺寸的图片没有被裁剪。浪费了用户加载流量,同时在一些虚拟滚动中,涉及到DOM创建和删除时,交互响应明显卡顿。在现有架构的前端上传产品图片:现有架构的缺失,使用错误的图片尺寸,难免会造成一些流量的浪费。这么大的流量也导致我们的页面加载速度不够快,尤其是有一些长列表的时候,因为图片尺寸的压缩让我们的页面有点卡顿。卡顿和加载速度慢会影响用户体验,降低产品的体验质量。上传和获取图片的改进方案前端获取图片,回退逻辑根据参数获取目标大小的图片,节省流量。优化前后前端图片压缩流程图选择优化前所有SKU后,页面等待10秒后弹出闪退提示。优化后,除了第一次加载数据外,有一个加载等待,后续的滚动和交互都没有卡顿。数据比对解决核心渲染卡顿问题,功能由可用变为不可用,用户体验大幅提升。遗留问题及风险遗留问题仅涉及用户经常使用的场景,不涉及图片全量;不能减少用户流量;少量增加客户端的运行内存。风险评估低于阈值的图像将不予处理;历史数据更新可能存在遗漏;对于部分低版本用户,可能无法使用,退回到老逻辑。核心代码的详细实现页面访问//viewimportAvatarWithZipfrom'../molecules/AvatarWithZip';-+关键组件实现详解组件实现//AvatarWithZipimportReact,{useState}from'react';importAvatar,{AvatarProps}from'antd/lib/avatar';从'@/hooks/use-avatar-zipper'导入useAvatarZipper;接口PropTypes扩展AvatarProps{openZip:boolean;}constAvatarWithZip:React.FC=(props)=>{const{openZip,src,size,...otherProps}=props;const[localSrc,setLocalSrc]=useState(openZip?'':src);constresize=useAvatarZipper(大小为数字);React.useEffect(()=>{if(!openZip){return;}if(typeofsrc==='string'){resize(src).then(url=>{setLocalSrc(url);});}elseif(typeofsrc==='object'){setLocalSrc(src);}//eslint-disable-next-linereact-hooks/exhaustive-deps},[src,openZip]);return();};exportdefaultReact.memo(AvatarWithZip);对应用封装的hooksimportReactfrom'react';importLRUfrom'lru-cache';从'@/workers/avatar-zip.worker.ts'导入AvatarZipWorker;输入UseAvatarZipper=(src:string)=>Promise;constMAX_IAMGE_SIZE=1024;constzippedCache=newLRU({max:500,});constworker=newAvatarZipWorker();constuseAvatarZipper:(size?:number)=>UseAvatarZipper=(size=32)=>{constoffScreen=React.useRef(新OffscreenCanvas(大小,大小));constoffScreenCtx=React.useRef(offScreen.current.getContext('2d'));React.useEffect(()=>{offScreen.current=newOffscreenCanvas(size,size);offScreenCtx.current=offScreen.current.getContext('2d');worker.postMessage({type:'init',payload:size});},[尺寸]);const调整大小:(src:string,max?:number)=>Promise=React.useCallback((src,max=MAX_IAMGE_SIZE)=>newPromise((resolve,reject)=>{constmessageHandler=(事件: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)asstring);return()=>{};}worker.postMessage({type:'zip',payload:{src,max}});worker.addEventListener('message',messageHandler);return()=>{worker.removeEventListener('message',messageHandler);};}),[]);returnresize;};exportdefaultuseAvatarZipper;worker实际constctx:Worker=selfasany;interfaceMessageData{类型:字符串;有效负载:任何;}导出接口MessageReturnData{类型:字符串;有效负载:任何;}让offScreen:OffscreenCanvas;让offScreenCtx:OffscreenCanvasRenderingContext2D|null;//响应来自父线程的消息ctx.addEventListener('message',async(event:MessageEvent)=>{const{data}=event;const{type,payload}=data;if(type==='init'){offScreen=newOffscreenCanvas(payload,payload);offScreenCtx=offScreen.getContext('2d');}if(type==='zip'){const{src,max}=payload;try{if(!offScreenCtx){throwError();}constres=awaitfetch(src);constsrcBlob=awaitres.blob();constimageBitmap=awaitcreateImageBitmap(srcBlob);if(Math.max(imageBitmap.width,imageBitmap.height)<=max){ctx.postMessage({origin:src,dist:src,});}constsize=offScreen.width;offScreenCtx.clearRect(0,0,大小,大小);offScreenCtx.drawImage(imageBitmap,0,0,size,size);constblobUrl=awaitoffScreen.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',},}//类型声明模块'*.worker.ts'{classWebpackWorkerextendsWorker{constructor();}导出默认WebpackWorker;}总结总的来说实现思路并不复杂,主要是对worker不熟悉的同学可以参考这篇worker的实现以及降级处理等边界问题的处理。就这些了,希望对大家有所启发。如果我说错了,请指正,谢谢。