From f2e9c0ab23d9c9327d7d4d4d457e0f17d97b0387 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 1 Jun 2024 11:13:03 -0400 Subject: [PATCH] perf: Remove useComponentSize from image and video node render --- app/hooks/useComponentSize.ts | 4 +- app/scenes/Document/components/Document.tsx | 30 ++++-------- .../Document/components/MeasuredContainer.tsx | 29 +++++++++++ shared/editor/components/Image.tsx | 11 ++--- shared/editor/components/Video.tsx | 10 ++-- .../editor/components/hooks/useDragResize.ts | 49 ++++++++++++++----- 6 files changed, 87 insertions(+), 46 deletions(-) create mode 100644 app/scenes/Document/components/MeasuredContainer.tsx diff --git a/app/hooks/useComponentSize.ts b/app/hooks/useComponentSize.ts index 700a8fb5a..36d650833 100644 --- a/app/hooks/useComponentSize.ts +++ b/app/hooks/useComponentSize.ts @@ -1,6 +1,8 @@ import { useState, useEffect } from "react"; -export default function useComponentSize(ref: React.RefObject): { +export default function useComponentSize( + ref: React.RefObject +): { width: number; height: number; } { diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index 1a505d53d..faaf89b5a 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -17,7 +17,6 @@ import { import { toast } from "sonner"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; -import useComponentSize from "@shared/editor/components/hooks/useComponentSize"; import { s } from "@shared/styles"; import { NavigationNode } from "@shared/types"; import { ProsemirrorHelper, Heading } from "@shared/utils/ProsemirrorHelper"; @@ -54,6 +53,7 @@ import Editor from "./Editor"; import Header from "./Header"; import KeyboardShortcutsButton from "./KeyboardShortcutsButton"; import MarkAsViewed from "./MarkAsViewed"; +import { MeasuredContainer } from "./MeasuredContainer"; import Notices from "./Notices"; import PublicReferences from "./PublicReferences"; import References from "./References"; @@ -438,7 +438,9 @@ class DocumentScene extends React.Component { } }} /> - { onSave={this.onSave} headings={this.headings} /> - { )} - + {isShare && !parseDomain(window.location.origin).custom && !auth.user && ( @@ -564,7 +568,7 @@ class DocumentScene extends React.Component { )} - + ); } @@ -622,20 +626,4 @@ const MaxWidth = styled(Flex)` `}; `; -const FullWidthContainer = (props: React.ComponentProps) => { - const ref = React.useRef(null); - const rect = useComponentSize(ref.current); - - return ( - - ); -}; - export default withTranslation()(withStores(withRouter(DocumentScene))); diff --git a/app/scenes/Document/components/MeasuredContainer.tsx b/app/scenes/Document/components/MeasuredContainer.tsx new file mode 100644 index 000000000..d25409930 --- /dev/null +++ b/app/scenes/Document/components/MeasuredContainer.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import useComponentSize from "@shared/editor/components/hooks/useComponentSize"; + +export const MeasuredContainer = ({ + as: As, + name, + children, + ...rest +}: { + as: T; + name: string; + children?: React.ReactNode; +} & React.ComponentProps) => { + const ref = React.useRef(null); + const rect = useComponentSize(ref.current); + + return ( + + {children} + + ); +}; diff --git a/shared/editor/components/Image.tsx b/shared/editor/components/Image.tsx index 464a75019..34b378325 100644 --- a/shared/editor/components/Image.tsx +++ b/shared/editor/components/Image.tsx @@ -7,7 +7,6 @@ import { sanitizeUrl } from "../../utils/urls"; import { ComponentProps } from "../types"; import ImageZoom from "./ImageZoom"; import { ResizeLeft, ResizeRight } from "./ResizeHandle"; -import useComponentSize from "./hooks/useComponentSize"; import useDragResize from "./hooks/useDragResize"; type Props = ComponentProps & { @@ -29,18 +28,16 @@ const Image = (props: Props) => { const [loaded, setLoaded] = React.useState(false); const [naturalWidth, setNaturalWidth] = React.useState(node.attrs.width); const [naturalHeight, setNaturalHeight] = React.useState(node.attrs.height); - const documentBounds = useComponentSize(props.view.dom); - const maxWidth = documentBounds.width; + const ref = React.useRef(null); const { width, height, setSize, handlePointerDown, dragging } = useDragResize( { width: node.attrs.width ?? naturalWidth, height: node.attrs.height ?? naturalHeight, - minWidth: documentBounds.width * 0.1, - maxWidth, naturalWidth, naturalHeight, - gridWidth: documentBounds.width / 20, + gridSnap: 5, onChangeSize, + ref, } ); @@ -61,7 +58,7 @@ const Image = (props: Props) => { : { width: width || "auto" }; return ( -
+
(null); const isResizable = !!onChangeSize; const { width, height, setSize, handlePointerDown, dragging } = useDragResize( { width: node.attrs.width ?? naturalWidth, height: node.attrs.height ?? naturalHeight, - minWidth: documentBounds.width * 0.1, - maxWidth: documentBounds.width, naturalWidth, naturalHeight, - gridWidth: documentBounds.width / 20, + gridSnap: 5, onChangeSize, + ref, } ); @@ -48,7 +46,7 @@ export default function Video(props: Props) { }; return ( -
+
(event: React.PointerEvent) => void; + /** Handler to set the new size of the element from outside. */ setSize: React.Dispatch>; + /** Whether the element is currently being resized. */ dragging: boolean; + /** The current width of the element. */ width: number; + /** The current height of the element. */ height?: number; }; -type Props = { +type Params = { + /** Callback triggered when the image is resized */ onChangeSize?: undefined | ((size: SizeState) => void); + /** The initial width of the element. */ width: number; + /** The initial height of the element. */ height: number; + /** The natural width of the element. */ naturalWidth: number; + /** The natural height of the element. */ naturalHeight: number; - minWidth: number; - maxWidth: number; - gridWidth: number; + /** The percentage of the grid to snap the element to. */ + gridSnap: 5; + /** A reference to the element being resized. */ + ref: React.RefObject; }; -export default function useDragResize(props: Props): ReturnValue { +export default function useDragResize(props: Params): ReturnValue { const [size, setSize] = React.useState({ width: props.width, height: props.height, }); + const [maxWidth, setMaxWidth] = React.useState(Infinity); const [offset, setOffset] = React.useState(0); const [sizeAtDragStart, setSizeAtDragStart] = React.useState(size); const [dragging, setDragging] = React.useState(); const isResizable = !!props.onChangeSize; - const constrainWidth = (width: number) => - Math.round(Math.min(props.maxWidth, Math.max(width, props.minWidth))); + const constrainWidth = (width: number, max: number) => { + const minWidth = Math.min(props.naturalWidth, (props.gridSnap / 100) * max); + return Math.round(Math.min(max, Math.max(width, minWidth))); + }; const handlePointerMove = (event: PointerEvent) => { event.preventDefault(); @@ -48,10 +65,10 @@ export default function useDragResize(props: Props): ReturnValue { diff = event.pageX - offset; } + const gridWidth = (props.gridSnap / 100) * maxWidth; const newWidth = sizeAtDragStart.width + diff * 2; - const widthOnGrid = - Math.round(newWidth / props.gridWidth) * props.gridWidth; - const constrainedWidth = constrainWidth(widthOnGrid); + const widthOnGrid = Math.round(newWidth / gridWidth) * gridWidth; + const constrainedWidth = constrainWidth(widthOnGrid, maxWidth); const aspectRatio = props.naturalHeight / props.naturalWidth; setSize({ @@ -88,8 +105,18 @@ export default function useDragResize(props: Props): ReturnValue { (event: React.PointerEvent) => { event.preventDefault(); event.stopPropagation(); + + // Calculate constraints once at the start of dragging as it's relatively expensive operation + const max = props.ref.current + ? parseInt( + getComputedStyle(props.ref.current).getPropertyValue( + "--document-width" + ) + ) + : Infinity; + setMaxWidth(max); setSizeAtDragStart({ - width: constrainWidth(size.width), + width: constrainWidth(size.width, max), height: size.height, }); setOffset(event.pageX);