Wątek przeniesiony 2023-09-15 11:39 z JavaScript przez Riddle.

Zmiana rozmiaru Iframe laguje

0

Hej, mam funkcje ktora zmienia mi rozmiar zdjecia oraz iframe w ReactJS. I o ile dla zdjecia wszystko dziala git, tak dla iframe zmiana rozmiaru jest wolniejsza i po puszczeniu lewego przycisku myszki dalej nastepuje resizing zamiast sie skonczyc. Poznizej moj kod oraz video pokazujace co sie dzieje. Czyli dla iframe po kliknieciu myszki zmiana nastepuje zmiana dosc wolno i gdy chce zmniejszyc rozmiar to w ogole sie nie da, i dopiero po dluzszym czasie moge dowolnie zmieniac rozmiar przy czym tutaj na video juz akurat puscilem lewy przycisk myszy a resizing sie dalej odbywal (ten plynny). Co powinienem zmienic?

https://vimeo.com/853650919

import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import React, { useEffect, useRef, useState } from "react";

// ! had to manage this state outside of the component because `useState` isn't fast enough and creates problem cause
// ! the function is getting old data even though new data is set by `useState` before the execution of function
let lastClientX: number;

const setLastClientX = (x: number) => {
	lastClientX = x;
};

interface WidthAndHeight {
	width: number;
	height: number;
}

export function IframeNodeView({ node, updateAttributes }: NodeViewProps) {
	const [aspectRatio, setAspectRatio] = useState(0);

	const [proseMirrorContainerWidth, setProseMirrorContainerWidth] = useState(0);

	const resizableImgRef = useRef<HTMLIFrameElement | null>(null);

	const mediaSetupOnLoad = () => {
		// ! TODO: move this to extension storage
		const proseMirrorContainerDiv = document.querySelector(".ProseMirror");

		if (proseMirrorContainerDiv)
			setProseMirrorContainerWidth(proseMirrorContainerDiv?.clientWidth);

		// When the media has loaded
		if (!resizableImgRef.current) return;

		resizableImgRef.current.onload = () => {
			// Aspect Ratio from its original size
			setAspectRatio(
				Number((resizableImgRef.current! as HTMLIFrameElement)?.width) /
					Number((resizableImgRef.current as HTMLIFrameElement).height)
			);
		};
	};

	useEffect(() => {
		mediaSetupOnLoad();
	});

	// const [isHorizontalResizeActive, setIsHorizontalResizeActive] =
	// 	useState(false);

	const limitWidthOrHeightToFiftyPixels = ({ width, height }: WidthAndHeight) =>
		width < 100 || height < 100;

	const documentHorizontalMouseMove = (e: MouseEvent) => {
		setTimeout(() => onHorizontalMouseMove(e));
	};

	const startHorizontalResize = (e: { clientX: number }) => {
		console.log("Start");
		// setIsHorizontalResizeActive(true);
		lastClientX = e.clientX;

		setTimeout(() => {
			document.addEventListener("mousemove", documentHorizontalMouseMove);
			document.addEventListener("mouseup", stopHorizontalResize);
		});
	};

	const stopHorizontalResize = () => {
		console.log("Stop");

		// setIsHorizontalResizeActive(false);
		lastClientX = -1;

		document.removeEventListener("mousemove", documentHorizontalMouseMove);
		document.removeEventListener("mouseup", stopHorizontalResize);
	};

	const onHorizontalResize = (
		directionOfMouseMove: "right" | "left",
		diff: number
	) => {
		if (!resizableImgRef.current) {
			console.error("Media ref is undefined|null", {
				resizableImg: resizableImgRef.current,
			});
			return;
		}

		const currentMediaDimensions = {
			width: resizableImgRef.current?.width,
			height: resizableImgRef.current?.height,
		};

		const newMediaDimensions = {
			width: -1,
			height: -1,
		};

		if (directionOfMouseMove === "left") {
			newMediaDimensions.width =
				Number(currentMediaDimensions.width) - Math.abs(diff);
		} else {
			newMediaDimensions.width =
				Number(currentMediaDimensions.width) + Math.abs(diff);
		}

		if (newMediaDimensions.width > proseMirrorContainerWidth)
			newMediaDimensions.width = proseMirrorContainerWidth;

		newMediaDimensions.height = newMediaDimensions.width / aspectRatio;

		if (limitWidthOrHeightToFiftyPixels(newMediaDimensions)) return;

		updateAttributes(newMediaDimensions);
	};

	const onHorizontalMouseMove = (e: MouseEvent) => {
		if (lastClientX === -1) return;

		const { clientX } = e;

		const diff = lastClientX - clientX;

		if (diff === 0) return;

		const directionOfMouseMove: "left" | "right" = diff > 0 ? "left" : "right";

		setTimeout(() => {
			onHorizontalResize(directionOfMouseMove, Math.abs(diff));
			lastClientX = clientX;
		});
	};

	let isWidthInPercentages =
		typeof node.attrs.width === "string" && node.attrs.width.endsWith("%");

	return (
		<NodeViewWrapper
			as="div"
			className="iframe-node-view group"
			contentEditable={false}
		>
			<iframe
				// contentEditable={false}
				src={node.attrs.src}
				ref={resizableImgRef as any}
				// className="rounded-lg"
				width={isWidthInPercentages ? "100%" : node.attrs.width}
				height={node.attrs.height}
				allowFullScreen={true}
				frameBorder={0}
				marginWidth={0}
				marginHeight={0}
			/>

			<div
				contentEditable={false}
				className="horizontal-resize-handle group-hover:bg-black group-hover:border-2 group-hover:border-white"
				title="Resize"
				onClick={({ clientX }) => setLastClientX(clientX)}
				onMouseDown={startHorizontalResize}
				onMouseUp={stopHorizontalResize}
			/>
		</NodeViewWrapper>
	);
}
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import React, { useEffect, useRef, useState } from "react";

// ! had to manage this state outside of the component because `useState` isn't fast enough and creates problem cause
// ! the function is getting old data even though new data is set by `useState` before the execution of function
let lastClientX: number;

const setLastClientX = (x: number) => {
	lastClientX = x;
};

interface WidthAndHeight {
	width: number;
	height: number;
}

export function IframeNodeView({ node, updateAttributes }: NodeViewProps) {
	const [aspectRatio, setAspectRatio] = useState(0);

	const [proseMirrorContainerWidth, setProseMirrorContainerWidth] = useState(0);

	const resizableImgRef = useRef<HTMLIFrameElement | null>(null);

	const mediaSetupOnLoad = () => {
		// ! TODO: move this to extension storage
		const proseMirrorContainerDiv = document.querySelector(".ProseMirror");

		if (proseMirrorContainerDiv)
			setProseMirrorContainerWidth(proseMirrorContainerDiv?.clientWidth);

		// When the media has loaded
		if (!resizableImgRef.current) return;

		resizableImgRef.current.onload = () => {
			// Aspect Ratio from its original size
			setAspectRatio(
				Number((resizableImgRef.current! as HTMLIFrameElement)?.width) /
					Number((resizableImgRef.current as HTMLIFrameElement).height)
			);
		};
	};

	useEffect(() => {
		mediaSetupOnLoad();
	});

	// const [isHorizontalResizeActive, setIsHorizontalResizeActive] =
	// 	useState(false);

	const limitWidthOrHeightToFiftyPixels = ({ width, height }: WidthAndHeight) =>
		width < 100 || height < 100;

	const documentHorizontalMouseMove = (e: MouseEvent) => {
		setTimeout(() => onHorizontalMouseMove(e));
	};

	const startHorizontalResize = (e: { clientX: number }) => {
		console.log("Start");
		// setIsHorizontalResizeActive(true);
		lastClientX = e.clientX;

		setTimeout(() => {
			document.addEventListener("mousemove", documentHorizontalMouseMove);
			document.addEventListener("mouseup", stopHorizontalResize);
		});
	};

	const stopHorizontalResize = () => {
		console.log("Stop");

		// setIsHorizontalResizeActive(false);
		lastClientX = -1;

		document.removeEventListener("mousemove", documentHorizontalMouseMove);
		document.removeEventListener("mouseup", stopHorizontalResize);
	};

	const onHorizontalResize = (
		directionOfMouseMove: "right" | "left",
		diff: number
	) => {
		if (!resizableImgRef.current) {
			console.error("Media ref is undefined|null", {
				resizableImg: resizableImgRef.current,
			});
			return;
		}

		const currentMediaDimensions = {
			width: resizableImgRef.current?.width,
			height: resizableImgRef.current?.height,
		};

		const newMediaDimensions = {
			width: -1,
			height: -1,
		};

		if (directionOfMouseMove === "left") {
			newMediaDimensions.width =
				Number(currentMediaDimensions.width) - Math.abs(diff);
		} else {
			newMediaDimensions.width =
				Number(currentMediaDimensions.width) + Math.abs(diff);
		}

		if (newMediaDimensions.width > proseMirrorContainerWidth)
			newMediaDimensions.width = proseMirrorContainerWidth;

		newMediaDimensions.height = newMediaDimensions.width / aspectRatio;

		if (limitWidthOrHeightToFiftyPixels(newMediaDimensions)) return;

		updateAttributes(newMediaDimensions);
	};

	const onHorizontalMouseMove = (e: MouseEvent) => {
		if (lastClientX === -1) return;

		const { clientX } = e;

		const diff = lastClientX - clientX;

		if (diff === 0) return;

		const directionOfMouseMove: "left" | "right" = diff > 0 ? "left" : "right";

		setTimeout(() => {
			onHorizontalResize(directionOfMouseMove, Math.abs(diff));
			lastClientX = clientX;
		});
	};

	let isWidthInPercentages =
		typeof node.attrs.width === "string" && node.attrs.width.endsWith("%");

	return (
		<NodeViewWrapper
			as="div"
			className="iframe-node-view group"
			contentEditable={false}
		>
			<iframe
				// contentEditable={false}
				src={node.attrs.src}
				ref={resizableImgRef as any}
				// className="rounded-lg"
				width={isWidthInPercentages ? "100%" : node.attrs.width}
				height={node.attrs.height}
				allowFullScreen={true}
				frameBorder={0}
				marginWidth={0}
				marginHeight={0}
			/>

			<div
				contentEditable={false}
				className="horizontal-resize-handle group-hover:bg-black group-hover:border-2 group-hover:border-white"
				title="Resize"
				onClick={({ clientX }) => setLastClientX(clientX)}
				onMouseDown={startHorizontalResize}
				onMouseUp={stopHorizontalResize}
			/>
		</NodeViewWrapper>
	);
}
0

A żeby usprawnić renderowanie proponuję tak:

  • pamiętaj, że coś takiego jak ref istnieje w reakcie - czasem warto przechować jakiś stan, który kompletnie nie jest potrzebny do renderowania
  • zamiast setTimeout z onHorizontalMouseMove używaj requestAnimationFrame
  • przed requestAnimationFrame ustaw sobie np. refa requested.current = true
  • w callbacku obsługującym faktyczną zmianę rozmiar, na samym końcu daj requested.current = false
  • nie zezwalaj na kolejny requestAnimationFrame jeżeli requested.current jest true (i/lub stosuj cancelAnimationFrame)

Nie wczytywałem się w kod na 100%, ani go nie próbowałem odpalić, więc nie podam dokładnego rozwiązania, ale tutaj widzę największe problemy w Twoim kodzie "na sucho". requestAnimationFrame bardziej naturalnie pod animacje zakolejkuje Ci to, co chcesz zrobić, a ten ref requested spowoduje, że nie spróbujesz obsłużyć dwóch lub więcej zmian rozmiaru w trakcie jednej klatki rysowania (mouse move może zostać odpalone kilka razy w trakcie jednej klatki na ekranie), co powinno odciążyć zbędne obliczenia rzeczy, które i tak się nie pokażą na ekranie oraz powinno zapobiec efektowi, że skończysz ruszać myszą, a tu nadal się coś zmienia

Ale może być też tak, że przerenderowanie tego iframe wymaga czasu i zawsze będzie to lekko lagować, w tym wypadku może dałoby się zrobić pustego diva zamiast iframe (iframe ukryj konkretnie) na czas zmiany rozmiaru, który będzie tylko "ramką", ew jakiś placeholder z tekstem w środku? Coś w rodzaju tego przełącznika w Windows: - szczególnie przydatnego x lat temu, kiedy komputer mógł słabo znosić takie przesuwanie czy właśnie zmianę rozmiaru.

1 użytkowników online, w tym zalogowanych: 0, gości: 1