almessadi.
العودة إلى الفهرس

عندما يتفوق WebGL على DOM في رسم واجهات المستخدم المعقدة_

كيفية التعرف على متى يتوقف DOM و SVG و Canvas عن التوسع، ومتى يصبح WebGL هو المسار الأفضل للرسم في الواجهات المعقدة التي تحتوي على بيانات كثيفة.

تاريخ النشر4 مارس 2024
وقت القراءة22 min read

معظم مشكلات أداء الواجهة الأمامية لا تتطلب WebGL.

إذا كنت تقوم بتحريك مجموعة من البطاقات، أو تلميحات الأدوات، أو حتى رسم بياني مشغول بشكل معتدل، فإن DOM عادة ما يكون هو التجسيد الصحيح. من الأسهل تصحيح الأخطاء، وأسهل لجعلها متاحة، وأيضًا من الأسهل الحفاظ على تكاملها مع بقية المنتج الخاص بك.

يصبح WebGL مثيرًا للاهتمام عندما يقضي المتصفح وقتًا أطول في إدارة الآلاف من الكائنات البصرية أكثر مما تنفقه تطبيقك على منطق المنتج الفعلي. غالبًا ما يظهر ذلك على هيئة مزيج من هذه الأعراض:

  • ارتفاع أوقات الإطار أثناء إعادة حساب الأنماط أو الطلاء
  • تنمو عقد SVG أو استدعاءات رسم canvas إلى عشرات الآلاف
  • تصبح التفاعلات noticeably worse على أجهزة الكمبيوتر المحمولة العادية، وليس فقط على آلة التطوير الخاصة بك
  • واجهة المستخدم بسيطة بصريًا، ولكن عبء الرسم كبير

هذه هي النقطة التي تتوقف فيها "تحسين مكون React" عن كونها خطة جدية.

ماذا يتغير عند الانتقال إلى WebGL

مع رسم DOM أو SVG، يمتلك المتصفح الكثير من العمل من أجلك:

  • التخطيط
  • حل الأنماط
  • اختبار النقر
  • الطلاء
  • التركيب

لقد كان لهذا الراحة تكلفة. كل عنصر يشارك في خطوط أنابيب المتصفح التي مصممة في الأصل للوثائق أولًا ثم الرسومات ثانيًا.

يأخذ WebGL العكس. تقوم بتحميل الهندسة وبيانات لكل مثيل إلى مخازن GPU، وتكتب الظلال لإخبار GPU كيفية الرسم، وتبقي المتصفح بعيدًا عن المسار الساخن. تفقد الراحة، لكنك تكسب التحكم في الإنتاجية.

بالنسبة للمشاهد الكثيفة المتحركة، قد تكون هذه التجارة تستحق ذلك.

مشكلة الأداء التي تقوم بحلها فعليًا

الخطأ الشائع هو التفكير في أن "WebGL أسرع" كقاعدة عامة. هو ليس كذلك.

WebGL يكون أسرع عندما:

  • تحتوي المشهد على العديد من الكائنات البصرية المتشابهة
  • يمكن تمثيل تلك الكائنات كبيانات رقمية هيكلية
  • يكون العمل في معظمه رسمًا، أو استيفاءً، أو تحويلات، أو حسابات لونية

WebGL غالبًا ما تكون الأداة الخطأ عندما:

  • تكون المشهد في معظمه نصًا
  • تهم دلالات الوصول على مستوى العناصر
  • تحتاج سلوك التخطيط الأصلي للمتصفح
  • تكون التعقيد في استرجاع البيانات أو تقلب الحالة، وليس في الرسم

مثال جيد هو عرض تلغراف يحتوي على آلاف النقاط المتحركة. مثال سيئ هو صفحة تسويقية تحتوي على ستة كتل زخرفية.

إعداد WebGL بسيط

الخطوة الأولى هي فقط الحصول على سطح رسم متوقع:

const canvas = document.querySelector("canvas");
const gl = canvas?.getContext("webgl2");

if (!gl) {
  throw new Error("WebGL2 غير متاح في هذا المتصفح");
}

function resizeCanvas() {
  const dpr = window.devicePixelRatio || 1;
  const width = Math.floor(canvas.clientWidth * dpr);
  const height = Math.floor(canvas.clientHeight * dpr);

  if (canvas.width !== width || canvas.height !== height) {
    canvas.width = width;
    canvas.height = height;
    gl.viewport(0, 0, width, height);
  }
}

resizeCanvas();
window.addEventListener("resize", resizeCanvas);

هناك تفاصيــــل تهم هنا:

  • عليك تعيين حجم المخزن الاحتياطي باستخدام devicePixelRatio، وليس فقط بكسل CSS.
  • عليك استدعاء gl.viewport(...) بعد تغيير الحجم، أو سيظهر المشهد بأبعاد قديمة.

المكسب الحقيقي: الاحتفاظ بالبيانات على GPU

غالبًا ما يأتي أكبر تحسين في أداء WebGL ليس من ظل ذكي، بل من تجنب حركة المرور غير الضرورية بين وحدة المعالجة المركزية وGPU.

هذا هو نوع مسار التحديث الذي يقتل الأداء بهدوء:

function renderFrame(points: Float32Array) {
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
  gl.drawArrays(gl.POINTS, 0, points.length / 2);
}

إعادة تخصيص الموارد على المسار الساخن. هذا هو مكافئ GPU لإعادة بناء العالم في كل إطار.

نمط صحي هو تخصيص مرة واحدة والتحديث في مكانه:

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, MAX_POINTS * 2 * Float32Array.BYTES_PER_ELEMENT, gl.DYNAMIC_DRAW);

function updatePoints(points: Float32Array) {
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, points);
}

إذا كان العدد المحدد للنقاط محدودًا، فإن التخصيص المسبق هو عادة الخيار الصحيح.

الظلال هي مجرد برامج

يبدو WebGL مُرعبًا حتى تتوقف عن التفكير في الظلال كـ "سحر رسومي" وتبدأ في التفكير فيها كبرامج صغيرة ذات مدخلات ومخرجات صارمة.

يقرر ظل الرأس أين سيكون شيء ما على الشاشة:

#version 300 es
in vec2 a_position;
uniform vec2 u_resolution;

void main() {
  vec2 zeroToOne = a_position / u_resolution;
  vec2 zeroToTwo = zeroToOne * 2.0;
  vec2 clipSpace = zeroToTwo - 1.0;

  gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
  gl_PointSize = 4.0;
}

يقرر ظل التجزئة كيف تبدو البيكسلات الناتجة:

#version 300 es
precision highp float;

out vec4 outColor;

void main() {
  vec2 p = gl_PointCoord - vec2(0.5);
  float distanceFromCenter = length(p);
  float alpha = smoothstep(0.5, 0.0, distanceFromCenter);

  outColor = vec4(0.14, 0.72, 0.97, alpha);
}

هذا يكفي لرسم نقاط دائرية ناعمة دون الحاجة إلى طلب إدارة آلاف العناصر من DOM.

أين يتناسب React بعد

نقل الرسم إلى WebGL لا يعني إزالة React من التطبيق.

التقسيم النظيف هو:

  • تمتلك React عناصر التحكم، والفلاتر، والتخطيط، وتحميل البيانات
  • يمتلك WebGL سطح الرسم الكثيف

عادة ما يعني ذلك التعامل مع الكانفس كجزيرة إمبراطورية:

import { useEffect, useRef } from "react";

export function PointCloud({ points }: { points: Float32Array }) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const rendererRef = useRef<Renderer | null>(null);

  useEffect(() => {
    if (!canvasRef.current) return;
    rendererRef.current = new Renderer(canvasRef.current);

    return () => {
      rendererRef.current?.dispose();
      rendererRef.current = null;
    };
  }, []);

  useEffect(() => {
    rendererRef.current?.update(points);
  }, [points]);

  return <canvas ref={canvasRef} className="h-full w-full" />;
}

يحافظ هذا النمط على React خارج حلقة الرسم لكل إطار، وهو عادة ما تريده.

المشكلات التي ينسى الناس

غالبًا ما تفشل مشاريع WebGL لأسباب لا تتعلق بالرياضيات الظلية:

  • فقدان السياق حقيقي. تحتاج إلى التعامل مع webglcontextlost و webglcontextrestored.
  • الرسم النصي محرج. غالبًا ما تحتاج العلامات إلى طبقة ثانية أو نهج هجين مع DOM.
  • تصحيح الأخطاء أبطأ من العمل العادي في واجهة المستخدم.
  • الوصول لا يأتي مجانًا.
  • تسرب ذاكرة GPU لا يزال تسربًا، حتى لو لم تجعل أدوات Chrome DevTools ذلك واضحًا.

إذا كان المنتج يحتاج إلى هيكل DOM دلالي، وتصفح لوحة المفاتيح، ومحتوى متوافق مع القارئ للشاشة لكل عقدة بصرية، فهي عادة ما تكون السطح النقي لـ WebGL الشكل الخطأ.

قاعدة عملية

استخدم أبسط نموذج رسم يمكنه تلبية عبء العمل.

ابدأ بـ DOM أو SVG إذا استطعت. انتقل إلى Canvas عندما تحتاج إلى حلقة رسم أكثر انخفاضًا. انتقل إلى WebGL عندما تكون المشكلة رسمًا على نطاق واسع ويمكن أن تساعد GPU فعليًا.

هذا التسلسل ليس عقيدة. إنها مجرد طريقة جيدة لتجنب حل مشكلة وثيقة باستخدام خط أنابيب الرسوميات.

قراءة إضافية