Skip to content

Commit

Permalink
fix: improve perf of <Resize> by creating the canvas in a side effect
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan committed Jun 12, 2024
1 parent 07454a1 commit 464b451
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 32 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"check:format": "prettier . --check",
"check:lint": "turbo run lint --continue -- --quiet",
"check:react-exhaustive-deps": "turbo run lint --continue -- --quiet --rule 'react-hooks/exhaustive-deps: [error, {additionalHooks: \"(useMemoObservable|useObservableCallback)\"}]'",
"check:react-compiler": "eslint --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --rule 'react-compiler/react-compiler: [warn]' --ignore-path .eslintignore.react-compiler --max-warnings 35 .",
"check:react-compiler": "eslint --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --rule 'react-compiler/react-compiler: [warn]' --ignore-path .eslintignore.react-compiler --max-warnings 34 .",
"check:test": "run-s test -- --silent",
"check:types": "tsc && turbo run check:types --filter='./packages/*' --filter='./packages/@sanity/*'",
"chore:format:fix": "prettier --cache --write .",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-shadow */
import {type ReactNode, useCallback, useEffect, useState} from 'react'
import {type ReactNode, useLayoutEffect, useRef, useState} from 'react'

export interface ResizeProps {
image: HTMLImageElement
Expand All @@ -10,41 +9,57 @@ export interface ResizeProps {

export function Resize(props: ResizeProps): any {
const {image, maxHeight, maxWidth, children} = props
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const [ready, setReady] = useState(false)

const [canvas] = useState<HTMLCanvasElement>(() => {
const canvasElement = document.createElement('canvas')
canvasElement.style.display = 'none'
return canvasElement
})
useEffect(() => {
document.body.appendChild(canvas)
return () => {
document.body.removeChild(canvas)
/**
* The useLayoutEffect is used here intentionally.
* Since the first render doesn't have a canvas element yet we return `null` instead of calling `children` so that `ImageTool` don't have to deal with
* the initial render not having a canvas element.
* Now, the flow is that first ImageTool will render a loading state, then it will render <Resize> and expect it to have a canvas that
* renders the provided image.
* If we use `useEffect` there will be a flash where <ImageTool> just finished rendering loading,
* then it will render with nothing, causing a jump,
* and finally it renders the image inside the canvas.
* By using `useLayoutEffect` we ensure that the intermediary state where there is no canvas doesn't paint in the browser,
* React blocks it, runs render again, this time we have a canvas element that got setup inside the effect and assigned to the ref,
* and then we render the image inside the canvas.
* No flash, no jumps, just a smooth transition from loading to image.
*/
useLayoutEffect(() => {
if (!canvasRef.current) {
const canvasElement = document.createElement('canvas')
canvasElement.style.display = 'none'
canvasRef.current = canvasElement
setReady(true)
}
}, [canvas])

const resize = useCallback(
(image: HTMLImageElement, maxHeight: number, maxWidth: number) => {
const ratio = image.width / image.height
const width = Math.min(image.width, maxWidth)
const height = Math.min(image.height, maxHeight)
const ratio = image.width / image.height
const width = Math.min(image.width, maxWidth)
const height = Math.min(image.height, maxHeight)

const landscape = image.width > image.height
const targetWidth = landscape ? width : height * ratio
const targetHeight = landscape ? width / ratio : height

const landscape = image.width > image.height
const targetWidth = landscape ? width : height * ratio
const targetHeight = landscape ? width / ratio : height
canvasRef.current.width = targetWidth
canvasRef.current.height = targetHeight

canvas.width = targetWidth
canvas.height = targetHeight
const ctx = canvasRef.current.getContext('2d')
if (ctx) {
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, targetWidth, targetHeight)
}

const ctx = canvas.getContext('2d')
if (ctx) {
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, targetWidth, targetHeight)
}
const node = canvasRef.current
document.body.appendChild(node)
return () => {
document.body.removeChild(node)
}
}, [image, maxHeight, maxWidth])

return canvas
},
[canvas],
)
if (!canvasRef.current || !ready) {
return null
}

return children(resize(image, maxHeight, maxWidth))
return children(canvasRef.current)
}

0 comments on commit 464b451

Please sign in to comment.