گلوگاههای عملکردی در پردازش گرافیکی وب (WebGL و WebGPU)
اجرای روان صحنههای سهبعدی پیچیده در مرورگر، نیازمند توزیع متوازن بار پردازشی میان پردازنده مرکزی (CPU) و پردازنده گرافیکی (GPU) است. در بسیاری از برنامههای گرافیکی تحت وب، کندی و افت فریم ناشی از ضعف کارت گرافیک نیست، بلکه نتیجه سربار سنگین درایور، جابهجایی بیش از حد دادهها در پهنای باند حافظه، یا ایجاد مسدودی در رزوه اصلی (Main Thread) جاوااسکریپت است.
بهینهسازی گرافیک وب شامل سه محور اساسی زیر است که رعایت هر یک برای تثبیت نرخ ۶۰ یا ۱۲۰ فریم بر ثانیه ضروری است:
-
محدودیتهای CPU (Draw Call Bottleneck):
هر بار ارسال دستور رندر از CPU به GPU باعث تغییر وضعیت در ماشین حالت WebGL میشود. افزایش این دستورات به بیش از چند صد عدد، افت فریم محسوس ایجاد میکند.
-
مصرف پهنای باند حافظه و VRAM:
تکسچرهای با ابعاد نامتعارف و مدلهای حجیم، حافظه کارت گرافیک را پر کرده و منجر به کرش تب مرورگر به دلیل محدودیتهای سیستم عامل میشوند.
-
سربار پرکردن پیکسلها (Fill-Rate و Overdraw):
محاسبات سنگین سایهزنهای فرگمنت بر روی پیکسلهایی که در نهایت پشت اشیاء دیگر پنهان میمانند، چرخه پردازشی GPU را هدر میدهد.
۱. ادغام دستورات رندر با Instanced Rendering
هنگامی که صدها یا هزاران جسم با ساختار هندسی و متریال یکسان (مانند درختان، ذرات، ساختمانها یا قطعات مکانیکی) در صحنه حضور دارند، به جای فراخوانی مجزای متد رندر برای هر کدام، از رندرینگ نمونهبرداریشده (Instancing) استفاده میشود. در این شیوه، دادههای موقعیت، چرخش، مقیاس و رنگ در قالب بافرهای ماتریسی به GPU ارسال شده و کل مجموعه تنها با یک فراخوانی رسم (Single Draw Call) ترسیم میگردد.
import * as THREE from 'three';
const count = 10000;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ roughness: 0.4, metalness: 0.1 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
const position = new THREE.Vector3();
const rotation = new THREE.Euler();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3();
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
position.set(
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100
);
rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
quaternion.setFromEuler(rotation);
scale.setScalar(Math.random() * 0.5 + 0.5);
matrix.compose(position, quaternion, scale);
instancedMesh.setMatrixAt(i, matrix);
color.setHSL(i / count, 0.7, 0.5);
instancedMesh.setColorAt(i, color);
}
instancedMesh.instanceMatrix.needsUpdate = true;
if (instancedMesh.instanceColor) {
instancedMesh.instanceColor.needsUpdate = true;
}
۲. بهینهسازی ساختار هندسی با سطوح جزئیات (LOD) و الگوریتمهای فشردهسازی
نمایش مشهای چندصدهزار راسی زمانی که جسم در فاصله دوری از دوربین قرار دارد، توانی غیرضروری از کارت گرافیک مصرف میکند. تکنیک سطح جزئیات (Level of Detail یا LOD) با توجه به فاصله جسم از ناظر، نسخههای سبکتر هندسه را جایگزین میکند. علاوه بر این، انتقال هندسه از سرور به کلاینت باید با بهرهگیری از استانداردهایی چون DRACO یا Meshopt انجام شود تا حجم دانلود و پردازش بافرهای داده تا ۸۰ درصد کاهش یابد.
import * as THREE from 'three';
const lod = new THREE.LOD();
const material = new THREE.MeshStandardMaterial({ roughness: 0.5 });
const highDetailGeo = new THREE.IcosahedronGeometry(2, 6);
const highMesh = new THREE.Mesh(highDetailGeo, material);
lod.addLevel(highMesh, 0);
const mediumDetailGeo = new THREE.IcosahedronGeometry(2, 3);
const mediumMesh = new THREE.Mesh(mediumDetailGeo, material);
lod.addLevel(mediumMesh, 25);
const lowDetailGeo = new THREE.IcosahedronGeometry(2, 1);
const lowMesh = new THREE.Mesh(lowDetailGeo, material);
lod.addLevel(lowMesh, 60);
const billboardGeo = new THREE.PlaneGeometry(4, 4);
const billboardMat = new THREE.MeshBasicMaterial({ color: 0x888888 });
const billboardMesh = new THREE.Mesh(billboardGeo, billboardMat);
lod.addLevel(billboardMesh, 120);
۳. مدیریت حافظه و بافتها با استانداردهای KTX2 و Basis Universal
بارگذاری تصاویر سنتی مثل PNG یا JPEG در مرورگر مستلزم دیکد شدن به فرمت بیتمپ ۳۲ بیتی خام در حافظه رم و سپس ارسال به VRAM است که حجم بسیار بالایی از حافظه کارت گرافیک را اشغال میکند. فرمتهای فشردهسازی سختافزاری GPU (مانند BCn در ویندوز، ASTC در موبایل و ETC2) بدون نیاز به دیکد شدن کامل، مستقیماً توسط پردازنده گرافیکی خوانده میشوند. فرمت کانتینر KTX2 به همراه ترنسکدر Basis Universal این تطابق سختافزاری را به صورت آنی در سطح مرورگر فراهم میسازد.
import * as THREE from 'three';
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('/basis/');
ktx2Loader.detectSupport(renderer);
const gltfLoader = new GLTFLoader();
gltfLoader.setKTX2Loader(ktx2Loader);
gltfLoader.load('/models/optimized_scene.glb', (gltf) => {
gltf.scene.traverse((child) => {
if (child.isMesh && child.material.map) {
child.material.map.anisotropy = renderer.capabilities.getMaxAnisotropy();
child.material.needsUpdate = true;
}
});
});
۴. غربالگری دید و ساختار سلسلهمراتبی (Frustum Culling)
اجزایی از صحنه که خارج از مخروط دید دوربین (View Frustum) قرار دارند نباید در چرخه ترسیم وارد شوند. هرچند موتورهای سهبعدی به صورت پیشفرض محاسبات تقاطع Bounding Box را انجام میدهند، در صحنههای وسیع انجام این محاسبات به صورت سلسلهمراتبی و با استفاده از Bounding Sphere کارایی محاسباتی بالاتری ارائه میدهد و از بررسی تکتک مشها در درخت صحنه جلوگیری میکند.
import * as THREE from 'three';
export class CustomCuller {
constructor(camera) {
this.camera = camera;
this.frustum = new THREE.Frustum();
this.projScreenMatrix = new THREE.Matrix4();
}
update() {
this.projScreenMatrix.multiplyMatrices(
this.camera.projectionMatrix,
this.camera.matrixWorldInverse
);
this.frustum.setFromProjectionMatrix(this.projScreenMatrix);
}
cull(rootNode) {
this.update();
rootNode.traverse((object) => {
if (object.isMesh) {
if (!object.geometry.boundingSphere) {
object.geometry.computeBoundingSphere();
}
const sphere = object.geometry.boundingSphere.clone();
sphere.applyMatrix4(object.matrixWorld);
object.visible = this.frustum.intersectsSphere(sphere);
}
});
}
}
۵. انتقال پردازشهای رندر به Web Worker از طریق OffscreenCanvas
در برنامههای وب تکصفحهای مدرن، رقابت فریمورکهای رابط کاربری، منطق تجاری و رندرینگ سهبعدی بر سر منابع نخ اصلی (Main Thread) منجر به افت فریمهای تصادفی (Jank) میشود. با انتقال مالکیت تگ کانوَس به یک Web Worker به واسطه رابط OffscreenCanvas، چرخه رندرینگ به صورت کاملاً ایزوله اجرا شده و عملیات سنگین DOM یا محاسبات دیتابیسی مانع از تداوم انیمیشنهای گرافیکی نمیشوند.
import * as THREE from 'three';
self.onmessage = function (event) {
if (event.data.type === 'init') {
const canvas = event.data.canvas;
const width = event.data.width;
const height = event.data.height;
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: false });
renderer.setSize(width, height, false);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
camera.position.z = 5;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff88, wireframe: true });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function render() {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
};
۶. معماری نوین WebGPU: پایپلاینهای حالتناپذیر و شتابدهی موازی
برخلاف WebGL که بر اساس ماشین حالت فراگیر (Global State Machine) عمل کرده و هزینههای اعتبارسنجی مداوم بر پردازنده تحمیل میکند، استانداردهای مدرن WebGPU از الگوهای مستقیم سختافزاری نظیر Vulkan، Metal و DirectX 12 پیروی میکنند. اشیاء پایپلاین (RenderPipeline) در زمان بارگذاری کامپایل و فریز میشوند؛ بدین ترتیب فراخوانی دستورات ترسیم با حداقل سربار CPU و بازدهی حداکثری هستههای پردازش موازی GPU اجرا میگردد.
export async function initWebGPURenderer(canvas) {
if (!navigator.gpu) {
throw new Error('WebGPU not supported on this browser.');
}
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
if (!adapter) {
throw new Error('No suitable GPU adapter found.');
}
const device = await adapter.requestDevice();
const context = canvas.getContext('webgpu');
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: presentationFormat,
alphaMode: 'premultiplied'
});
const shaderModule = device.createShaderModule({
code: `
@vertex
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>(0.5, -0.5)
);
return vec4<f32>(pos[in_vertex_index], 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(0.2, 0.8, 0.4, 1.0);
}
`
});
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vs_main'
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [{ format: presentationFormat }]
},
primitive: {
topology: 'triangle-list'
}
});
function frame() {
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPassDescriptor = {
colorAttachments: [{
view: textureView,
clearValue: { r: 0.05, g: 0.05, b: 0.05, a: 1.0 },
loadOp: 'clear',
storeOp: 'store'
}]
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.draw(3, 1, 0, 0);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
چکلیست ممیزی و ارزیابی عملکرد سهبعدی در وب
پیش از انتشار نهایی اپلیکیشنهای تعاملی، سنجش معیارهای زیر با استفاده از ابزارهای Spector.js و تب Performance مرورگر کروم جهت اطمینان از تجربه پایدار کاربر پیشنهاد میشود:
| شاخص کارایی |
حد استاندارد |
راهکار رفع گلوگاه |
| تعداد Draw Calls |
کمتر از ۱۰۰ در هر فریم |
ادغام ژئومتریها یا بهکارگیری InstancedMesh |
| حجم حافظه VRAM تکسچرها |
کمتر از ۲۵۶ مگابایت |
تبدیل تمام بافتها به KTX2 با فشردهسازی Basis |
| تعداد کل راسها (Vertices) |
کمتر از ۵۰۰٬۰۰۰ در صحنه |
پیادهسازی پلههای LOD و الگوریتمهای Decimation |
| زمان فریم نخ اصلی (Main Thread) |
کمتر از ۱۶٫۶ میلیثانیه |
انتقال منطق به OffscreenCanvas و وبورکرها |