Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/entities/scene/lib/scene-timing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const SCENE_TIMING_GLB_READY_PREFIX = 'scene:glb-ready:';
const SCENE_TIMING_FIRST_FRAME_MARK = 'scene:first-frame';
const SCENE_TIMING_FIRST_CONTENT_FRAME_MARK = 'scene:first-content-frame';

/**
* ๊ฐ™์€ ์ด๋ฆ„์˜ mark๊ฐ€ ์ด๋ฏธ ์žˆ์œผ๋ฉด ๋‹ค์‹œ ์ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
* Suspense๋Š” ๊ฐ™์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ํ‰๊ฐ€ํ•˜๋ฏ€๋กœ, ์ตœ์ดˆ ์‹œ์ ๋งŒ ๋‚จ๊ฒจ์•ผ ์˜๋ฏธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
*/
const markOnce = (name: string): void => {
if (typeof performance === 'undefined') return;
if (performance.getEntriesByName(name, 'mark').length > 0) return;
Comment thread
pcwadarong marked this conversation as resolved.

performance.mark(name);
};

/**
* GLB ํ•˜๋‚˜๊ฐ€ ํŒŒ์‹ฑ๊ณผ KTX2 transcode๋ฅผ ๋งˆ์ณ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์ง„ ์‹œ์ ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
*
* ๋‹ค์šด๋กœ๋“œ ๊ตฌ๊ฐ„์€ ๋ธŒ๋ผ์šฐ์ €์˜ Resource Timing์— ์ด๋ฏธ ์žˆ์œผ๋ฏ€๋กœ ๋”ฐ๋กœ ์žฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
* ์—ฌ๊ธฐ์„œ ์žฌ์•ผ ํ•˜๋Š” ๊ฒƒ์€ **๋ฐ”์ดํŠธ๊ฐ€ ๋„์ฐฉํ•œ ๋’ค์— ์ผ์–ด๋‚˜๋Š” ์ผ**๋ฟ์ž…๋‹ˆ๋‹ค.
*/
export const markSceneGlbReady = (path: string): void => {
markOnce(`${SCENE_TIMING_GLB_READY_PREFIX}${path}`);
};

/**
* ์บ”๋ฒ„์Šค๊ฐ€ ์‚ด์•„๋‚˜ ์ฒซ ํ”„๋ ˆ์ž„์„ ๊ทธ๋ฆฐ ์‹œ์ ์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ์”ฌ์€ ์•„์ง ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
*/
export const markSceneFirstFrame = (): void => {
markOnce(SCENE_TIMING_FIRST_FRAME_MARK);
};

/**
* ์”ฌ ๋‚ด์šฉ(์บ๋ฆญํ„ฐยท์†Œํ’ˆ)์ด ์‹ค์ œ๋กœ ๊ทธ๋ ค์ง„ ์ฒซ ํ”„๋ ˆ์ž„์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
*
* {@link markSceneFirstFrame}๊ณผ ๋ฐ˜๋“œ์‹œ ๋‚˜๋ˆ ์•ผ ํ•ฉ๋‹ˆ๋‹ค โ€” `<Canvas>`๋Š” GLB๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ 
* ๋นˆ ์”ฌ์œผ๋กœ ๋จผ์ € ๊ทธ๋ ค์ง€๋ฏ€๋กœ(์‹ค์ธก ์•ฝ 430ms ์•ž์„ฌ), ๊ทธ ์‹œ์ ์„ "์ฒซ ํ”„๋ ˆ์ž„"์œผ๋กœ ์“ฐ๋ฉด
* ๋กœ๋”ฉ ๋น„์šฉ ์ „์ฒด๊ฐ€ ์ˆซ์ž์—์„œ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
*/
export const markSceneFirstContentFrame = (): void => {
markOnce(SCENE_TIMING_FIRST_CONTENT_FRAME_MARK);
};
8 changes: 7 additions & 1 deletion src/entities/scene/model/use-scene-gltf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useGLTF } from '@react-three/drei';
import { useThree } from '@react-three/fiber';

import { markSceneGlbReady } from '@/entities/scene/lib/scene-timing';
import { resolveKtx2Loader } from '@/shared/lib/three/ktx2-loader';

// GLB๋Š” ์ „๋ถ€ Meshopt๋กœ ์••์ถ•๋˜์–ด Draco ๋””์ฝ”๋”๊ฐ€ ์‹ค์ œ๋กœ ์“ฐ์ด์ง„ ์•Š์ง€๋งŒ,
Expand All @@ -23,10 +24,15 @@ type StdlibKtx2Loader = Parameters<Parameters<GltfExtendLoader>[0]['setKTX2Loade
export const useSceneGltf = (path: string): SceneGltf => {
const gl = useThree(state => state.gl);

return useGLTF(path, true, true, loader => {
const gltf = useGLTF(path, true, true, loader => {
// drei์˜ GLTFLoader๋Š” three-stdlib ๊ฒƒ์ด๋ผ KTX2Loader ํƒ€์ž…๋„ three-stdlib ์„ ์–ธ์„ ์š”๊ตฌํ•œ๋‹ค.
// three-stdlib์€ ์ง์ ‘ ์˜์กด์„ฑ์ด ์•„๋‹ˆ๋ฏ€๋กœ(drei์˜ ์ „์ด ์˜์กด) three ๋ณธ์ฒด์˜ KTX2Loader๋ฅผ ์“ฐ๊ณ ,
// ๊ตฌ์กฐ๋งŒ ๊ฐ™์œผ๋ฉด ๋˜๋Š” ์ž๋ฆฌ๋ผ ์—ฌ๊ธฐ์„œ ํ•œ ๋ฒˆ๋งŒ ์ขํ˜€ ์ค€๋‹ค.
loader.setKTX2Loader(resolveKtx2Loader(gl) as unknown as StdlibKtx2Loader);
});

// Suspense๊ฐ€ ํ’€๋ฆฐ ๋’ค ์ด ์ค„์— ์ฒ˜์Œ ๋„๋‹ฌํ•œ ์‹œ์ ์ด ๊ณง "ํŒŒ์‹ฑยทtranscode๊ฐ€ ๋๋‚œ ์‹œ์ "์ด๋‹ค.
markSceneGlbReady(path);

return gltf;
};
60 changes: 60 additions & 0 deletions src/entities/scene/ui/scene-timing-probe.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { render } from '@testing-library/react';
import React from 'react';

import { SceneTimingProbe } from '@/entities/scene/ui/scene-timing-probe';

const frameCallbacks: (() => void)[] = [];

vi.mock('@react-three/fiber', () => ({
useFrame: (callback: () => void) => {
frameCallbacks.push(callback);
},
}));

/** ๋ Œ๋” ๋ฃจํ”„๊ฐ€ ๋„๋Š” ๊ฒƒ์„ ํ‰๋‚ด ๋‚ธ๋‹ค. jsdom์—๋Š” ์‹ค์ œ ํ”„๋ ˆ์ž„์ด ์—†๋‹ค. */
const runFrames = (count: number) => {
for (let index = 0; index < count; index += 1) {
frameCallbacks.forEach(callback => {
callback();
});
}
};

const markNames = () =>
performance
.getEntriesByType('mark')
.map(entry => entry.name)
.filter(name => name.startsWith('scene:'));

describe('SceneTimingProbe', () => {
beforeEach(() => {
frameCallbacks.length = 0;
performance.clearMarks();
});

it('ํ”„๋ ˆ์ž„์ด ์—ฌ๋Ÿฌ ๋ฒˆ ๋Œ์•„๋„ ์ฒซ ํ”„๋ ˆ์ž„ mark๋ฅผ ํ•œ ๋ฒˆ๋งŒ ๋‚จ๊ธด๋‹ค', () => {
render(<SceneTimingProbe />);
runFrames(3);

expect(markNames()).toEqual(['scene:first-frame']);
});

it('isContent๋ฉด ์”ฌ ๋‚ด์šฉ์ด ๊ทธ๋ ค์ง„ ํ”„๋ ˆ์ž„์œผ๋กœ ๊ธฐ๋กํ•œ๋‹ค', () => {
render(<SceneTimingProbe isContent />);
runFrames(3);

expect(markNames()).toEqual(['scene:first-content-frame']);
});

it('๋‘ ํ”„๋กœ๋ธŒ๊ฐ€ ํ•จ๊ป˜ ์žˆ์œผ๋ฉด ๋นˆ ์บ”๋ฒ„์Šค์™€ ์”ฌ ๋‚ด์šฉ ์‹œ์ ์„ ๊ฐ๊ฐ ๋‚จ๊ธด๋‹ค', () => {
render(
<>
<SceneTimingProbe />
<SceneTimingProbe isContent />
</>,
);
runFrames(2);

expect(markNames().sort()).toEqual(['scene:first-content-frame', 'scene:first-frame']);
});
});
41 changes: 41 additions & 0 deletions src/entities/scene/ui/scene-timing-probe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';

import { markSceneFirstContentFrame, markSceneFirstFrame } from '@/entities/scene/lib/scene-timing';

type SceneTimingProbeProps = {
/**
* `true`๋ฉด ์”ฌ ๋‚ด์šฉ์ด ๊ทธ๋ ค์ง„ ์ฒซ ํ”„๋ ˆ์ž„์œผ๋กœ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค.
* Suspense ์•ˆ์ชฝ(GLB๊ฐ€ ์ค€๋น„๋œ ๋’ค ๋งˆ์šดํŠธ๋˜๋Š” ์ž๋ฆฌ)์— ๋‘” ์ธ์Šคํ„ด์Šค๋งŒ ์ด ๊ฐ’์„ ์ผญ๋‹ˆ๋‹ค.
*/
readonly isContent?: boolean;
};

/**
* ์”ฌ์ด ์ฒ˜์Œ ๋ Œ๋”๋œ ํ”„๋ ˆ์ž„์— `performance.mark`๋ฅผ ๋‚จ๊น๋‹ˆ๋‹ค. ํ™”๋ฉด์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ๊ทธ๋ฆฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
*
* `<Canvas>` ์•ˆ์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค โ€” `useFrame`์€ ์‹ค์ œ๋กœ ๊ทธ๋ ค์ง€๋Š” ํ”„๋ ˆ์ž„์—์„œ๋งŒ ๋Œ๊ธฐ ๋•Œ๋ฌธ์—
* "๋กœ๋“œ๊ฐ€ ๋๋‚œ ์‹œ์ "์ด ์•„๋‹ˆ๋ผ "ํ™”๋ฉด์— ๋‚˜์˜จ ์‹œ์ "์„ ์žก์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
* ๋‚จ๊ธด mark๋Š” DevTools Performance ํŒจ๋„ ํƒ€์ž„๋ผ์ธ์— ๊ทธ๋Œ€๋กœ ํ‘œ์‹œ๋˜๋ฏ€๋กœ ๋ณ„๋„ ์ง‘๊ณ„๋Š” ๋‘์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
*/
export const SceneTimingProbe = ({ isContent = false }: SceneTimingProbeProps) => {
Comment thread
pcwadarong marked this conversation as resolved.
const hasMarkedRef = useRef(false);

useFrame(() => {
if (hasMarkedRef.current) return;

hasMarkedRef.current = true;

if (isContent) {
markSceneFirstContentFrame();

return;
}

markSceneFirstFrame();
});

return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ vi.mock('@react-three/fiber', () => ({
</div>
);
},
// SceneTimingProbe๊ฐ€ ์ฒซ ํ”„๋ ˆ์ž„์„ ๊ธฐ๋กํ•˜๋Š” ๋ฐ ์“ด๋‹ค. jsdom์—๋Š” ๋ Œ๋” ๋ฃจํ”„๊ฐ€ ์—†์œผ๋ฏ€๋กœ no-op.
useFrame: () => undefined,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}));

vi.mock('@react-three/drei', () => ({
Expand Down
3 changes: 3 additions & 0 deletions src/widgets/home-hero-scene/ui/home-hero-stage-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { css, cx } from 'styled-system/css';
import type { SceneBreakpoint, SceneViewportMode } from '@/entities/scene/model/breakpointConfig';
import { BASS_MODEL_PATH, TABLE_MODEL_PATH } from '@/entities/scene/model/preloadGLB';
import { SceneProp } from '@/entities/scene/ui/scene-prop';
import { SceneTimingProbe } from '@/entities/scene/ui/scene-timing-probe';
import { useBassAudio } from '@/features/audio/model/use-bass-audio';
import { scrollHomeHeroToProjects } from '@/features/interaction/model/scroll-home-hero-to-projects';
import { SceneInteractionController } from '@/features/interaction/ui/scene-interaction-controller';
Expand Down Expand Up @@ -151,6 +152,7 @@ export const HomeHeroStageCanvas = ({
}}
>
<HomeHeroStageLights />
<SceneTimingProbe />
<HomeHeroCameraRig
currentBP={currentBP}
interactionDisabledProgressThreshold={interactionDisabledProgressThreshold}
Expand All @@ -164,6 +166,7 @@ export const HomeHeroStageCanvas = ({
/>
<Suspense fallback={null}>
<Preload all />
<SceneTimingProbe isContent />
<HomeHeroStageReadyBridge
isReady
onReadyChange={onSceneReadyChange ?? noopBooleanHandler}
Expand Down
Loading