[Perf] Introduce Offscreen Canvas for blurred thumbs
This commit is contained in:
parent
2b773d7e85
commit
f2e8768123
@ -120,7 +120,7 @@ const File: FC<OwnProps> = ({
|
||||
{withThumb && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', 'canvas-blur-setup', thumbClassNames)}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -157,7 +157,7 @@ const GifButton: FC<OwnProps> = ({
|
||||
{withThumb && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className="thumbnail canvas-blur-setup"
|
||||
className="thumbnail"
|
||||
/>
|
||||
)}
|
||||
{previewBlobUrl && !isVideoReady && (
|
||||
|
||||
@ -63,7 +63,7 @@ const MediaSpoiler: FC<OwnProps> = ({
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={buildClassName(styles.canvas, 'canvas-blur-setup')}
|
||||
className={styles.canvas}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
|
||||
@ -123,7 +123,7 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
content = (
|
||||
<>
|
||||
{isBlurredThumb ? (
|
||||
<canvas ref={blurredThumbCanvasRef} className="thumb canvas-blur-setup" />
|
||||
<canvas ref={blurredThumbCanvasRef} className="thumb" />
|
||||
) : (
|
||||
<img src={previewBlobUrl} draggable={false} className="thumb" alt="" />
|
||||
)}
|
||||
|
||||
@ -84,7 +84,7 @@ function BaseStory({
|
||||
onClick={isConnected ? handleClick : undefined}
|
||||
>
|
||||
{!isExpired && isPreview && (
|
||||
<canvas ref={blurredBackgroundRef} className="thumbnail canvas-blur-setup blurred-bg" />
|
||||
<canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />
|
||||
)}
|
||||
{shouldRender && (
|
||||
<>
|
||||
|
||||
@ -216,7 +216,7 @@ const Photo = <T,>({
|
||||
onClick={isUploading ? undefined : handleClick}
|
||||
>
|
||||
{withBlurredBackground && (
|
||||
<canvas ref={blurredBackgroundRef} className="thumbnail canvas-blur-setup blurred-bg" />
|
||||
<canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />
|
||||
)}
|
||||
<img
|
||||
src={fullMediaData}
|
||||
@ -228,7 +228,7 @@ const Photo = <T,>({
|
||||
{withThumb && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', !noThumb && 'canvas-blur-setup', thumbClassNames)}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
/>
|
||||
)}
|
||||
{isProtected && <span className="protector" />}
|
||||
|
||||
@ -260,7 +260,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
{!shouldRenderSpoiler && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', 'canvas-blur-setup', thumbClassNames)}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -235,7 +235,7 @@ const Video = <T,>({
|
||||
onClick={isUploading ? undefined : (e) => handleClick(e)}
|
||||
>
|
||||
{withBlurredBackground && (
|
||||
<canvas ref={blurredBackgroundRef} className="thumbnail canvas-blur-setup blurred-bg" />
|
||||
<canvas ref={blurredBackgroundRef} className="thumbnail blurred-bg" />
|
||||
)}
|
||||
{isInline && (
|
||||
<OptimizedVideo
|
||||
@ -262,7 +262,7 @@ const Video = <T,>({
|
||||
{hasThumb && !isPreviewPreloaded && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', !noThumb && 'canvas-blur-setup', thumbClassNames)}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
/>
|
||||
)}
|
||||
{isProtected && <span className="protector" />}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { getMediaThumbUri, type MediaWithThumbs } from '../../../../global/helpers';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../../../util/windowEnvironment';
|
||||
|
||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
||||
import useCanvasBlur from '../../../../hooks/useCanvasBlur';
|
||||
import useOffscreenCanvasBlur from '../../../../hooks/useOffscreenCanvasBlur';
|
||||
|
||||
type CanvasBlurReturnType = ReturnType<typeof useCanvasBlur>;
|
||||
type CanvasBlurReturnType = ReturnType<typeof useOffscreenCanvasBlur>;
|
||||
|
||||
export default function useBlurredMediaThumbRef(
|
||||
forcedUri: string | undefined, isDisabled: boolean,
|
||||
@ -14,13 +12,7 @@ export default function useBlurredMediaThumbRef(
|
||||
media: MediaWithThumbs | string | undefined,
|
||||
isDisabled?: boolean,
|
||||
) {
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const dataUri = media ? typeof media === 'string' ? media : getMediaThumbUri(media) : undefined;
|
||||
|
||||
return useCanvasBlur(
|
||||
dataUri,
|
||||
isDisabled,
|
||||
isMobile && !IS_CANVAS_FILTER_SUPPORTED,
|
||||
);
|
||||
return useOffscreenCanvasBlur(dataUri, isDisabled);
|
||||
}
|
||||
|
||||
@ -52,8 +52,6 @@ export default function useCanvasBlur(
|
||||
|
||||
ctx.drawImage(img, -radius * 2, -radius * 2, width + radius * 4, height + radius * 4);
|
||||
|
||||
canvas.classList.remove('canvas-blur-setup');
|
||||
|
||||
if (!IS_CANVAS_FILTER_SUPPORTED) {
|
||||
fastBlur(ctx, 0, 0, width, height, radius, ITERATIONS);
|
||||
}
|
||||
|
||||
36
src/hooks/useOffscreenCanvasBlur.ts
Normal file
36
src/hooks/useOffscreenCanvasBlur.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { useLayoutEffect, useMemo, useRef } from '../lib/teact/teact';
|
||||
|
||||
import cycleRestrict from '../util/cycleRestrict';
|
||||
import launchMediaWorkers, { MAX_WORKERS } from '../util/launchMediaWorkers';
|
||||
|
||||
const RADIUS = 7;
|
||||
|
||||
let lastWorkerIndex = -1;
|
||||
|
||||
export default function useOffscreenCanvasBlur(
|
||||
dataUri?: string,
|
||||
isDisabled = false,
|
||||
radius = RADIUS,
|
||||
) {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const workerIndex = useMemo(() => cycleRestrict(MAX_WORKERS, ++lastWorkerIndex), []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!dataUri || isDisabled) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
|
||||
const { connector } = launchMediaWorkers()[workerIndex];
|
||||
connector.request({
|
||||
name: 'blurThumb',
|
||||
args: [offscreen, dataUri, radius],
|
||||
transferables: [offscreen],
|
||||
});
|
||||
}, [dataUri, isDisabled, radius, workerIndex]);
|
||||
|
||||
return canvasRef;
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
import '../rlottie/rlottie.worker';
|
||||
import '../video-preview/video-preview.worker';
|
||||
import '../offscreen-canvas/offscreen-canvas.worker';
|
||||
|
||||
import type { OffscreenCanvasApi } from '../offscreen-canvas/offscreen-canvas.worker';
|
||||
import type { RLottieApi } from '../rlottie/rlottie.worker';
|
||||
import type { VideoPreviewApi } from '../video-preview/video-preview.worker';
|
||||
|
||||
export type MediaWorkerApi =
|
||||
RLottieApi
|
||||
& VideoPreviewApi;
|
||||
& VideoPreviewApi
|
||||
& OffscreenCanvasApi;
|
||||
|
||||
33
src/lib/offscreen-canvas/offscreen-canvas.worker.ts
Normal file
33
src/lib/offscreen-canvas/offscreen-canvas.worker.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { createWorkerInterface } from '../../util/createPostMessageInterface';
|
||||
|
||||
export async function blurThumb(canvas: OffscreenCanvas, dataUri: string, radius: number) {
|
||||
const imageBitmap = await dataUriToImageBitmap(dataUri);
|
||||
const { width, height } = canvas;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
// Draw image twice to battle white-ish edges
|
||||
ctx.drawImage(imageBitmap, 0, 0, width, height);
|
||||
ctx.filter = `blur(${radius}px)`;
|
||||
ctx.drawImage(imageBitmap, 0, 0, width, height);
|
||||
}
|
||||
|
||||
function dataUriToImageBitmap(dataUri: string) {
|
||||
const byteString = atob(dataUri.split(',')[1]);
|
||||
const mimeString = dataUri.split(',')[0].split(':')[1].split(';')[0];
|
||||
const buffer = new ArrayBuffer(byteString.length);
|
||||
const dataArray = new Uint8Array(buffer);
|
||||
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
dataArray[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const blob = new Blob([buffer], { type: mimeString });
|
||||
|
||||
return createImageBitmap(blob);
|
||||
}
|
||||
|
||||
const api = { blurThumb };
|
||||
|
||||
createWorkerInterface(api, 'media');
|
||||
|
||||
export type OffscreenCanvasApi = typeof api;
|
||||
@ -201,10 +201,6 @@ body:not(.is-ios) {
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-blur-setup {
|
||||
will-change: width, height;
|
||||
}
|
||||
|
||||
.emoji-small {
|
||||
background: no-repeat;
|
||||
background-size: var(--emoji-size);
|
||||
|
||||
@ -97,6 +97,7 @@ export type RequestTypes<T extends InputRequestTypes> = Values<{
|
||||
[Name in keyof (T)]: {
|
||||
name: Name & string;
|
||||
args: Parameters<T[Name]>;
|
||||
transferables?: Transferable[];
|
||||
}
|
||||
}>;
|
||||
|
||||
@ -107,6 +108,8 @@ class ConnectorClass<T extends InputRequestTypes> {
|
||||
|
||||
private pendingPayloads: OriginPayload[] = [];
|
||||
|
||||
private pendingTransferables: Transferable[] = [];
|
||||
|
||||
constructor(
|
||||
public target: Worker,
|
||||
private onUpdate?: (update: ApiUpdate) => void,
|
||||
@ -127,12 +130,13 @@ class ConnectorClass<T extends InputRequestTypes> {
|
||||
|
||||
request(messageData: RequestTypes<T>) {
|
||||
const { requestStates, requestStatesByCallback } = this;
|
||||
const { transferables, ...restMessageData } = messageData;
|
||||
|
||||
const messageId = generateUniqueId();
|
||||
const payload: CallMethodPayload = {
|
||||
type: 'callMethod',
|
||||
messageId,
|
||||
...messageData,
|
||||
...restMessageData,
|
||||
};
|
||||
|
||||
const requestState = { messageId } as RequestState;
|
||||
@ -161,7 +165,7 @@ class ConnectorClass<T extends InputRequestTypes> {
|
||||
}
|
||||
});
|
||||
|
||||
this.postMessageOnTickEnd(payload);
|
||||
this.postMessageOnTickEnd(payload, transferables);
|
||||
|
||||
return promise;
|
||||
}
|
||||
@ -208,20 +212,25 @@ class ConnectorClass<T extends InputRequestTypes> {
|
||||
});
|
||||
}
|
||||
|
||||
private postMessageOnTickEnd(payload: OriginPayload) {
|
||||
private postMessageOnTickEnd(payload: OriginPayload, transferables?: Transferable[]) {
|
||||
this.pendingPayloads.push(payload);
|
||||
|
||||
if (transferables) {
|
||||
this.pendingTransferables.push(...transferables);
|
||||
}
|
||||
|
||||
this.postMessagesOnTickEnd();
|
||||
}
|
||||
|
||||
private postMessagesOnTickEnd = throttleWithTickEnd(() => {
|
||||
const { channel } = this;
|
||||
const payloads = this.pendingPayloads;
|
||||
const transferables = this.pendingTransferables;
|
||||
|
||||
this.pendingPayloads = [];
|
||||
this.pendingTransferables = [];
|
||||
|
||||
this.target.postMessage({
|
||||
channel: this.channel,
|
||||
payloads,
|
||||
});
|
||||
this.target.postMessage({ channel, payloads }, transferables);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -21,20 +21,6 @@ if (typeof File === 'undefined') {
|
||||
} as typeof File;
|
||||
}
|
||||
|
||||
export function dataUriToBlob(dataUri: string) {
|
||||
const arr = dataUri.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)![1];
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
|
||||
while (n--) {
|
||||
u8arr[n] = bstr.charCodeAt(n);
|
||||
}
|
||||
|
||||
return new Blob([u8arr], { type: mime });
|
||||
}
|
||||
|
||||
export function blobToDataUri(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user