Skip to content

๐Ÿชต 4. ์บ”๋ฒ„์Šค ์„ฑ๋Šฅ ์ตœ์ ํ™” ์‹œ๋„ โ€ requestAnimationFrame, throttle, offscreenCanvas

D.Joung edited this page Dec 5, 2024 · 2 revisions

๊ฒฐ๋ก  ์š”์•ฝ ( 12์›” 02์ผ ๊ธฐ์ค€)

  • requestAnimationFrame (๋ฏธ์ ์šฉ)
  • offscreenCanvas (๋ฏธ์ ์šฉ)
  • throttle (์‹œ์ž‘ ํŽ˜์ด์ง€ ์บ”๋ฒ„์Šค์—๋งŒ ์ ์šฉ)

๊ณผ์ •

  • drawingBuffer ๋ฐฐ์—ด์„ ๋งŒ๋“ค์–ด์„œ ๋“œ๋กœ์ž‰ ํ•  ์ž‘์—…๋“ค์„ ์ €์žฅํ•œ ํ›„, requestAnimationFrame์„ ํ†ตํ•ด ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ์ตœ์ ํ™” ๋กœ์ง์„ ์‹œ๋„ํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ƒ๋Œ€์˜ ์„ ์„ ๋ฐ›์•˜์„ ๋•Œ ์ „์ฒด ์„ ์„ redrawํ•˜๋Š” ๊ณผ์ •์—์„œ, ctx.stroke() ํ˜ธ์ถœ ๋‹จ์œ„๋กœ ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ๋ Œ๋”๋ง๋˜๋Š” ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด drawingBuffer์— ๋„ฃ์„ ๋•Œ type ๋ถ€์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค.
    • line or redraw
  • ์ดํ›„ OffscreenCanvas๋ฅผ ์‚ฌ์šฉํ•ด ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ์—์„œ ๋“œ๋กœ์ž‰ ๋กœ์ง์„ ์ˆ˜ํ–‰ ํ›„ ๋ณ‘ํ•ฉํ•˜๋Š” ๋ฐฉ์‹๊นŒ์ง€ ์‹œ๋„ํ•ด๋ดค์ง€๋งŒ, ํ•ด๋‹น ๋ฐฉ์‹์˜ ๊ฒฝ์šฐ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•  ๋•Œ ๋งˆ๋‹ค ํ™”๋ฉด ๊นœ๋นก์ž„์ด ๋ฐœ์ƒํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.
    • ๋ฆฌ๋ Œ๋”๋ง์ด ๋นˆ๋ฒˆํ•  ๋•Œ๋Š” ๋ถ€์ ํ•ฉํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

OffscreenCanvas ์‹œ๋„

useDrawingState.ts

  interface DrawingLine {
    type: 'line';
    data: DrawingData;
  }

  interface DrawingRedraw {
    type: 'redraw';
    data: DrawingData[];
  }

  type DrawingBuffer = DrawingLine | DrawingRedraw;

  const drawingBufferRef = useRef<DrawingBuffer[]>([]);

useDrawingOperation.ts & useDrawing.ts

  • drawStroke์— ๋ฐ”๋กœ ์ž‘์—…์„ ๋งก๊ธฐ๋Š” ๋Œ€์‹  drawingBuffer์— ์ €์žฅ ํ›„ requestAnimationFrame ๋ฉ”์†Œ๋“œ๋กœ ์ตœ์‹  ์ž‘์—… ์ˆœ์œผ๋กœ ์ˆ˜ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
state.drawingBufferRef.current.push({ type: 'line', data: updatedDrawing });

useDrawingOperation.ts

  useEffect(() => {
    let lastTime: DOMHighResTimeStamp = 0;
    const drawingBuffer = state.drawingBufferRef.current;

    const drawAnimation = (timestemp: DOMHighResTimeStamp) => {
      if (timestemp - lastTime > 16) {
        if (drawingBuffer.length >= 0) {
          const currentDraw = drawingBuffer.shift();
          if (currentDraw) {
            if (currentDraw.type === 'line') drawStroke(currentDraw.data);
            else if (currentDraw.type === 'redraw') {
              const drawDataList = currentDraw.data;
              const { ctx } = getCanvasContext(canvasRef);
              const offScreen = new OffscreenCanvas(MAINCANVAS_RESOLUTION_WIDTH, MAINCANVAS_RESOLUTION_HEIGHT);
              const worker = new Worker('canvasWorker.js');

              worker.postMessage({ canvas: offScreen, dataList: drawDataList }, [offScreen]);

              worker.onmessage = (event) => {
                const bitmap = event.data; // ImageBitmap ๊ฐ์ฒด
                ctx.drawImage(bitmap, 0, 0); // ๋ฉ”์ธ ์บ”๋ฒ„์Šค ์œ„์— ๋ณ‘ํ•ฉ
              };
            }
          }
        }

        lastTime = timestemp;
      }

      requestAnimationFrame(drawAnimation);
    };
    const aniId = requestAnimationFrame(drawAnimation);

    return () => {
      if (aniId) cancelAnimationFrame(aniId);
    };

canvasWorker.js

  • react ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” public ํด๋” ์•„๋ž˜์— ์žˆ์–ด์•ผ ์ •์ƒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.
self.onmessage = (event) => {
  const {canvas, dataList} = event.data;
  const ctx = canvas.getContext('2d');

  ctx.beginPath();
  dataList.forEach((drawingData) => {
    const { points, style } = drawingData;
    ctx.save();
    ctx.strokeStyle = style.color;
    ctx.lineWidth = style.width;
    points.forEach((point, idx) => {
      const { x, y } = point;
      if (idx === 0) ctx.moveTo(x, y);
      else ctx.lineTo(x, y);
    });
  });
  ctx.stroke();

  const bitmap = canvas.transferToImageBitmap();
  // ์ž‘์—… ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ ์ „์†ก
  self.postMessage(bitmap, [bitmap]);
};

requestAnimationFrame ์‹œ๋„ ์ตœ์ข… ์ฝ”๋“œ

  • ํ•˜์ง€๋งŒ ์ ์šฉ ์•ˆํ•˜๋Š๋‹ˆ ๋ชปํ•œ ๊ฒƒ ๊ฐ™์•„ ๋ฏธ์ ์šฉ ์ƒํƒœ๋กœ ๋กค๋ฐฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ์•„๋ฌด๋ž˜๋„ ํ”„๋ ˆ์ž„ ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋„ˆ๋ฌด ๋งŽ์ด ๋ถ€์—ฌํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  useEffect(() => {
    let lastTime: DOMHighResTimeStamp = 0;
    const drawingBuffer = state.drawingBufferRef.current;

    const drawAnimation = (timestemp: DOMHighResTimeStamp) => {
      if (timestemp - lastTime > 16) {
        if (drawingBuffer.length >= 0) {
          const currentDraw = drawingBuffer.shift();
          if (currentDraw) {
            if (currentDraw.type === 'line') drawStroke(currentDraw.data); //type์ด line์ผ ๊ฒฝ์šฐ๋Š” ์ผ๋ฐ˜ ๋“œ๋กœ์ž‰
            if (currentDraw.type === 'redraw') //type์ด redraw์ผ ๊ฒฝ์šฐ์—๋Š” ์ „์ฒด ๋“œ๋กœ์ž‰ ๋ผ์ธ๋“ค์˜ ๋ฐฐ์—ด
              currentDraw.data.forEach((drawingData) => {
                drawStroke(drawingData);
              });
          }
        }

        lastTime = timestemp;
      }

      requestAnimationFrame(drawAnimation);
    };
    const aniId = requestAnimationFrame(drawAnimation);

    return () => {
      if (aniId) cancelAnimationFrame(aniId);
    };
  }, [drawStroke, state.drawingBufferRef.current]);

Background Canvas Throttle ์ ์šฉ

  • ์‹œ์ž‘ํ™”๋ฉด background์— ๋งˆ์šฐ์Šค ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์„ ๊ตฌํ˜„ํ•œ ๋กœ์ง์— throttle์„ ์ ์šฉํ•ด ์ตœ์ ํ™”๋ฅผ ์‹œ๋„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • Test ํ™˜๊ฒฝ
    • ๊ฐค๋Ÿญ์‹œ๋ถ2 - intel CORE i7 - RAM 16GB - x64
    • windows 11 Home
    • chrome ๋ธŒ๋ผ์šฐ์ €

์ ์šฉ ์ „ ํ˜„ํ™ฉ ์ฒดํฌ

  • mousemove ๋ฐœ์ƒ ์ฃผ๊ธฐ

    • performance.now() ๋ฅผ ์‚ฌ์šฉํ•ด mousemove ์ด๋ฒคํŠธ ๊ฐ„๊ฒฉ์„ console.log๋กœ ์ถœ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค.

      • ์ค‘๊ฐ„์— 100๋‹จ์œ„ ๋„˜๋Š” ์ˆ˜๋“ค์€ ๋งˆ์šฐ์Šค๋ฅผ ์ž ๊น ๋ฉˆ์ท„๋‹ค๊ฐ€ ๋‹ค์‹œ ์›€์ง์˜€์„ ๋•Œ ๋ฐœ์ƒ
    • ๋Œ€๋žต 7~8ms ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

        currentTimestamp.current = performance.now();
        console.log(currentTimestamp.current - lastTimestamp.current);
        lastTimestamp.current = currentTimestamp.current;
  • ๊ฐ€์šด๋ฐ ๋กœ๊ณ  ๋ฐ ๋ฒ„ํŠผ ๊ธฐ์ค€์œผ๋กœ ๋‘ ๋ฐ”ํ€ด ๊ทธ๋ ธ์„ ๋•Œ ๋ฒ„ํผ ์† ์ขŒํ‘œ ์ˆ˜๋ฅผ ์ธก์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    • 400~600๊ฐœ ๊ฐ€๋Ÿ‰ ์ขŒํ‘œ๊ฐ€ ์Œ“์ž…๋‹ˆ๋‹ค.

performance throttle ์ ์šฉ ํ›„ (16ms ๊ฐ„๊ฒฉ์œผ๋กœ ์„ค์ •)

  • 16ms ์ด์ƒ ๊ฐ„๊ฒฉ์—์„œ๋Š” ๋“œ๋กœ์ž‰์ด ์‹œ๊ฐ์ ์œผ๋กœ ์กฐ๊ธˆ ๋ถˆ์•ˆ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ 16ms ๊ฐ„๊ฒฉ์œผ๋กœ ์ขŒํ‘œ๋ฅผ ์ƒ˜ํ”Œ๋งํ•ฉ๋‹ˆ๋‹ค.
    currentTimestamp.current = performance.now();
    if (currentTimestamp.current - lastTimestamp.current < 16) return;

    console.log(currentTimestamp.current - lastTimestamp.current); //๊ฐ„๊ฒฉ ์ถœ๋ ฅ
    lastTimestamp.current = currentTimestamp.current;
  • mousemove ๋ฐœ์ƒ ์ฃผ๊ธฐ

    • performance.now() ๋ฅผ ์‚ฌ์šฉํ•ด mousemove ์ด๋ฒคํŠธ ๊ฐ„๊ฒฉ์„ console.log๋กœ ์ถœ๋ ฅํ•ด๋ด…๋‹ˆ๋‹ค.
      • ์ค‘๊ฐ„์— 100๋‹จ์œ„ ๋„˜๋Š” ์ˆ˜๋“ค์€ ๋งˆ์šฐ์Šค๋ฅผ ์ž ๊น ๋ฉˆ์ท„๋‹ค๊ฐ€ ๋‹ค์‹œ ์›€์ง์˜€์„ ๋•Œ ๋ฐœ์ƒ
    • ๋Œ€๋žต 16~25ms ์‚ฌ์ด๋กœ ๋ฐœ์ƒ
  • ๊ฐ€์šด๋ฐ ๋กœ๊ณ  ๋ฐ ๋ฒ„ํŠผ ๊ธฐ์ค€์œผ๋กœ ๋‘ ๋ฐ”ํ€ด ๊ทธ๋ ธ์„ ๋•Œ ๋ฒ„ํผ ์† ์ขŒํ‘œ ์ˆ˜๋ฅผ ์ธก์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

    • 200~300๊ฐœ ๊ฐ€๋Ÿ‰ ์ขŒํ‘œ๊ฐ€ ์Œ“์˜€์Šต๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ PC ๊ธฐ์ค€ throttle, requestAnimationFrame ๋‘˜ ๋‹ค 16ms ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด ์„ ์ด ๋งค๋„๋Ÿฝ๊ฒŒ ์ด์–ด์ง€๋ฏ€๋กœ, ์ƒ˜ํ”Œ๋ง ๊ฐ„๊ฒฉ์ด 16ms ๋กœ ๊ทœ์น™์ ์œผ๋กœ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๐Ÿ˜Ž ์›จ๋ฒ ๋ฒ ๋ฒ ๋ฒฑ

๐Ÿ‘ฎ๐Ÿป ํŒ€ ๊ทœ์น™

๐Ÿ’ป ํ”„๋กœ์ ํŠธ

๐Ÿชต ์›จ๋ฒ ๋ฒฑ ๊ธฐ์ˆ ๋กœ๊ทธ

๐Ÿช„ ๋ฐ๋ชจ ๊ณต์œ 

๐Ÿ”„ ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๋ก

๐Ÿ“— ํšŒ์˜๋ก

Clone this wiki locally