Nowadays, lazy loading of pictures is common. However, the space occupation of general lazy loading of pictures often does not follow the size of the original picture. As a result, the overall position will change after the picture is loaded, and the experience is very bad. I believe everyone has seen the image loading methods such as Zhihu or Medium. From the beginning of loading to the completion of loading, the space occupation is always in one place. Coupled with the smooth transition, it is visually comfortable.
So I tried to do the same with lazy loading of pictures today, but there is a problem. The pictures in all my articles are not local, but distributed in various picture beds, and some even fail. I began to try to process at the front end. When the picture starts loading, I can get the relevant information of the picture header before the loading is completed. However, there is a problem. In this case, there is still no smoothing.
So he gave up the research results of an afternoon and began to redo the back end. In the back-end model, I added a new field to record various information of the picture. Each article may have more than one graph, so it should be an array.
After that, extract the picture links in all articles, then request data, analyze the pictures and record them in the database. In this way, it only needs one operation. Then, when the article updates the amount, trigger the hook.
back-end
The following code takes nestjs and typegoose as examples
Extract picture links in markdown
ts
1export const pickImagesFromMarkdown = (text: string) => { 2 const reg = /(?<=\!\[.*\]\()(.+)(?=\))/g 3 const images = [] as string[] 4 for (const r of text.matchAll(reg)) { 5 images.push(r[0]) 6 } 7 return images 8}
COPY
Get the picture and analyze it, using the http module and image size library of NestJS
ts
1import { imageSize } from 'image-size' 2import { HttpService } from '@nestjs/common' 3export const getOnlineImageSize = async (http: HttpService, image: string) => { 4 const { data } = await http 5 .get(image, { 6 responseType: 'arraybuffer', 7 }) 8 .toPromise() 9 const buffer = Buffer.from(data) 10 const size = imageSize(buffer) 11 return size 12}
COPY
Save to database
ts
1// base.service.ts 2// class WriteBaseService 3 4async RecordImageDimensions(id: string, socket?: SocketIO.Socket) { 5 const text = (await this.__model.findById(id).lean()).text 6 const images = pickImagesFromMarkdown(text) 7 const result = [] as ISizeCalculationResult[] 8 for await (const image of images) { 9 try { 10 this.logger.log('Get --> ' + image) 11 const size = await getOnlineImageSize(this.__http, image) 12 if (socket) { 13 socket.send( 14 gatewayMessageFormat( 15 EventTypes.IMAGE_FETCH, 16 'Get --> ' + image + JSON.stringify(size), 17 ), 18 ) 19 } 20 result.push(size) 21 } catch (e) { 22 this.logger.error(e.message) 23 if (socket) { 24 socket.send(gatewayMessageFormat(EventTypes.IMAGE_FETCH, e.message)) 25 } 26 result.push({ 27 width: undefined, 28 height: undefined, 29 type: undefined, 30 }) 31 } 32 } 33 34 await this.__model.updateOne( 35 { _id: id as any }, 36 // @ts-ignore 37 { $set: { images: result } }, 38 ) 39 }
COPY
Because some pictures may be 404, or the network is not good (domestic direct connection) https://raw.githubusercontent.com/ )Therefore, if an error is reported, you should also push it. Just leave it blank. The front end will handle it at that time.
ts
1;[this.postService, this.noteService, this.pageService].forEach( 2 async (s) => { 3 s.refreshImageSize(socket) 4 }, 5 ) 6 }
COPY
Finally, execute this method on all model s.
front end
The front part takes React as an example.
After processing, an images field is added to the data returned by the backend, such as.

Before rendering the picture, the front end first calculates the size to be rendered to the page according to the actual size, then determines the size of the placeholder, and removes or hides the placeholder after the picture is loaded.
The calculated dimensions can be referred to as follows:
ts
1const calculateDimensions = (width?: number, height?: number) => { 2 if (!width || !height) { 3 return { height: 300, width: undefined } 4 } 5 const MAX = { 6 width: document.getElementById('write')?.offsetWidth || 500, // Width of container 7 height: Infinity, // Optional maximum height 8 } 9 const dimensions = { width, height } 10 if (width > height && width > MAX.width) { 11 dimensions.width = MAX.width 12 dimensions.height = (MAX.width / width) * height 13 } else if (height === width) { 14 if (width <= MAX.width) { 15 dimensions.height = dimensions.width = height 16 } else { 17 dimensions.height = MAX.width 18 dimensions.width = dimensions.height 19 } 20 } 21 return dimensions 22}
COPY
Because the structure of Markdown rendering is complex, I use Context to transfer values. The rendering library I use is react Markdown, which can render each tag customized.
tsx
1const RenderImage: FC<{ src: string; alt?: string }> = ({ src, alt }) => { 2 const images = useContext(imageSizesContext) 3 const [cal, setCal] = useState({} as { height?: number; width?: number }) 4 useEffect(() => { 5 const size = images.shift() 6 const cal = calculateDimensions(size?.width, size?.height) 7 8 setCal(cal) 9 }, [images]) 10 if (typeof document === 'undefined') { 11 return null 12 } 13 14 return ( 15 <ImageLazyWithPopup 16 src={src} 17 alt={alt} 18 height={cal.height} 19 width={cal.width} 20 /> 21 ) 22}
COPY
The complete Image component can be found in
see. Contains excessive animation of the picture.