بهبود عملکرد HTML5 Canvas

معرفی

بوم HTML5 که به عنوان آزمایشی از اپل آغاز شد، گسترده ترین استاندارد پشتیبانی شده برای گرافیک حالت فوری دوبعدی در وب است. بسیاری از توسعه دهندگان اکنون برای طیف گسترده ای از پروژه های چند رسانه ای، تجسم ها و بازی ها به آن متکی هستند. با این حال، با افزایش پیچیدگی برنامه‌هایی که می‌سازیم، توسعه‌دهندگان ناخواسته به دیوار عملکرد ضربه می‌زنند. در مورد بهینه‌سازی عملکرد بوم، حکمت‌های منفصل زیادی وجود دارد. هدف این مقاله تثبیت بخشی از این بدنه در یک منبع قابل هضم تر برای توسعه دهندگان است. این مقاله شامل بهینه‌سازی‌های اساسی است که برای همه محیط‌های گرافیکی رایانه‌ای و همچنین تکنیک‌های خاص بوم که با بهبود پیاده‌سازی بوم تغییر می‌کنند، اعمال می‌شود. به طور خاص، از آنجایی که فروشندگان مرورگر شتاب GPU بوم را اجرا می‌کنند، برخی از تکنیک‌های عملکرد مشخص شده مورد بحث احتمالاً تأثیر کمتری خواهند داشت. در صورت لزوم به این موضوع اشاره خواهد شد. توجه داشته باشید که این مقاله به استفاده از بوم HTML5 نمی‌پردازد. برای این کار، این مقالات مربوط به بوم را در HTML5Rocks، این فصل در سایت Dive into HTML5 یا MDN Canvas بررسی کنید. آموزش

ازمایش عملکرد

برای پرداختن به دنیای به سرعت در حال تغییر بوم HTML5، آزمایش‌های JSPerf ( jsperf.com ) تأیید می‌کنند که هر بهینه‌سازی پیشنهادی همچنان کار می‌کند. JSPerf یک برنامه وب است که به توسعه دهندگان اجازه می دهد تا تست های عملکرد جاوا اسکریپت را بنویسند. هر آزمون بر روی نتیجه ای تمرکز می کند که شما در تلاش برای رسیدن به آن هستید (مثلاً پاک کردن بوم)، و شامل چندین رویکرد است که به یک نتیجه می رسد. JSPerf هر رویکرد را تا آنجا که ممکن است در یک بازه زمانی کوتاه اجرا می‌کند و از نظر آماری تعداد تکرار معنی‌داری در هر ثانیه ارائه می‌کند. نمرات بالاتر همیشه بهتر است! بازدیدکنندگان یک صفحه تست عملکرد JSPerf می‌توانند آزمایش را در مرورگر خود اجرا کنند و به JSPerf اجازه دهند نتایج تست نرمال‌شده را در Browserscope ( browserscope.org ) ذخیره کند. از آنجایی که تکنیک‌های بهینه‌سازی در این مقاله با یک نتیجه JSPerf پشتیبان‌گیری می‌شوند، می‌توانید برای مشاهده اطلاعات به‌روز در مورد اینکه آیا این تکنیک همچنان کاربرد دارد یا خیر، بازگردید. من یک برنامه کمکی کوچک نوشته ام که این نتایج را به صورت نمودار ارائه می کند که در سراسر این مقاله جاسازی شده است.

تمام نتایج عملکرد در این مقاله بر روی نسخه مرورگر کلید شده است. معلوم می‌شود که این یک محدودیت است، زیرا ما نمی‌دانیم مرورگر روی چه سیستم‌عاملی اجرا می‌شود، یا مهم‌تر از آن، اینکه آیا بوم HTML5 هنگام اجرای تست عملکرد، سرعت سخت‌افزاری داشت یا خیر. با مراجعه about:gpu در نوار آدرس می‌توانید متوجه شوید که آیا بوم HTML5 کروم شتاب سخت‌افزاری دارد یا خیر.

پیش نمایش به بوم خارج از صفحه نمایش

اگر در حال ترسیم اولیه‌های مشابه روی صفحه نمایش در فریم‌های متعدد هستید، همانطور که اغلب هنگام نوشتن یک بازی اتفاق می‌افتد، می‌توانید با پیش‌رندر کردن بخش‌های بزرگ صحنه، دستاوردهای عملکردی زیادی داشته باشید. پیش رندر به معنای استفاده از یک بوم (یا بوم) جدا از صفحه است که برای رندر کردن تصاویر موقت روی آن، و سپس رندر کردن بوم های خارج از صفحه نمایش بر روی بوم قابل مشاهده است. به عنوان مثال، فرض کنید ماریو را دوباره با سرعت 60 فریم در ثانیه ترسیم می کنید. می‌توانید کلاه، سبیل و «M» او را در هر فریم دوباره ترسیم کنید، یا قبل از اجرای انیمیشن، ماریو را از قبل رندر کنید. بدون پیش رندر:

// canvas, context are defined
function render() {
  drawMario(context);
  requestAnimationFrame(render);
}

پیش رندر:

var m_canvas = document.createElement('canvas');
m_canvas.width = 64;
m_canvas.height = 64;
var m_context = m_canvas.getContext('2d');
drawMario(m_context);

function render() {
  context.drawImage(m_canvas, 0, 0);
  requestAnimationFrame(render);
}

به استفاده از requestAnimationFrame توجه کنید که در بخش بعدی با جزئیات بیشتر مورد بحث قرار خواهد گرفت.

این تکنیک مخصوصاً زمانی مؤثر است که عملیات رندرینگ ( drawMario در مثال بالا) گران باشد. یک مثال خوب در این زمینه رندر متن است که عملیات بسیار گرانی است.

با این حال، عملکرد ضعیف مورد آزمایشی "از قبل رندر شده شل" است. هنگام پیش رندر، مهم است که مطمئن شوید بوم موقت شما به خوبی در اطراف تصویری که می‌کشید قرار می‌گیرد، در غیر این صورت افزایش عملکرد رندر خارج از صفحه با کاهش عملکرد کپی کردن یک بوم بزرگ روی بوم دیگر (که به عنوان یک بوم متفاوت متفاوت است) متعادل می‌شود. تابع اندازه هدف منبع). یک بوم راحت در تست بالا به سادگی کوچکتر است:

can2.width = 100;
can2.height = 40;

در مقایسه با شل که عملکرد ضعیف تری دارد:

can3.width = 300;
can3.height = 100;

تماس های دسته ای با هم

از آنجایی که ترسیم یک عملیات گران قیمت است، کارآمدتر است که ماشین حالت ترسیم را با مجموعه‌ای طولانی از دستورات بارگیری کنید و سپس همه آنها را در بافر ویدیو رها کنید.

به عنوان مثال، هنگام ترسیم چندین خط، کارآمدتر است که یک مسیر با تمام خطوط موجود در آن ایجاد کنید و آن را با یک فراخوانی ترسیم کنید. به عبارت دیگر، به جای ترسیم خطوط جداگانه:

for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.beginPath();
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
  context.stroke();
}

از رسم یک چند خط منفرد عملکرد بهتری دریافت می کنیم:

context.beginPath();
for (var i = 0; i < points.length - 1; i++) {
  var p1 = points[i];
  var p2 = points[i+1];
  context.moveTo(p1.x, p1.y);
  context.lineTo(p2.x, p2.y);
}
context.stroke();

این در دنیای بوم HTML5 نیز صدق می کند. به عنوان مثال، هنگام ترسیم یک مسیر پیچیده، بهتر است همه نقاط را در مسیر قرار دهید، نه اینکه بخش ها را به طور جداگانه رندر کنید ( jsperf ).

با این حال، توجه داشته باشید که در Canvas، یک استثنای مهم برای این قانون وجود دارد: اگر موارد اولیه درگیر در ترسیم شی مورد نظر دارای کادرهای محدود کوچکی باشند (مثلاً خطوط افقی و عمودی)، ممکن است در واقع ارائه جداگانه آنها کارآمدتر باشد. jsperf ).

از تغییرات غیر ضروری حالت بوم خودداری کنید

عنصر بوم HTML5 در بالای یک ماشین حالت اجرا می شود که مواردی مانند سبک های پر کردن و استروک و همچنین نقاط قبلی که مسیر فعلی را تشکیل می دهند را ردیابی می کند. هنگام تلاش برای بهینه سازی عملکرد گرافیکی، وسوسه انگیز است که صرفاً روی رندر گرافیکی تمرکز کنید. با این حال، دستکاری ماشین حالت همچنین می تواند هزینه های سربار عملکرد را به همراه داشته باشد. برای مثال، اگر از چندین رنگ پر برای رندر یک صحنه استفاده می‌کنید، رندر کردن بر اساس رنگ به جای قرار دادن روی بوم ارزان‌تر است. برای رندر کردن یک الگوی نواری، می‌توانید یک نوار را رندر کنید، رنگ‌ها را تغییر دهید، نوار بعدی را رندر کنید و غیره:

for (var i = 0; i < STRIPES; i++) {
  context.fillStyle = (i % 2 ? COLOR1 : COLOR2);
  context.fillRect(i * GAP, 0, GAP, 480);
}

یا تمام راه راه های فرد و سپس تمام راه راه های زوج را رندر کنید:

context.fillStyle = COLOR1;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2) * GAP, 0, GAP, 480);
}
context.fillStyle = COLOR2;
for (var i = 0; i < STRIPES/2; i++) {
  context.fillRect((i*2+1) * GAP, 0, GAP, 480);
}

همانطور که انتظار می رود، رویکرد درهم آمیخته کندتر است زیرا تغییر ماشین حالت گران است.

تنها تفاوت های صفحه نمایش را رندر کنید، نه وضعیت جدید را

همانطور که انتظار می رود، رندر کمتر بر روی صفحه نمایش ارزان تر از رندر بیشتر است. اگر تنها تفاوت‌های افزایشی بین ترسیم‌های مجدد دارید، می‌توانید تنها با ترسیم تفاوت، عملکرد قابل توجهی را افزایش دهید. به عبارت دیگر، به جای پاک کردن کل صفحه قبل از کشیدن:

context.fillRect(0, 0, canvas.width, canvas.height);

کادر مرزی ترسیم شده را دنبال کنید و فقط آن را پاک کنید.

context.fillRect(last.x, last.y, last.width, last.height);

اگر با گرافیک کامپیوتری آشنایی دارید، ممکن است این تکنیک را به عنوان «مناطق دوباره ترسیم کنید» نیز بشناسید، جایی که کادر کراندار قبلی رندر شده ذخیره می شود و سپس در هر رندر پاک می شود. این تکنیک همچنین در زمینه‌های رندر مبتنی بر پیکسل اعمال می‌شود، همانطور که در این سخنرانی شبیه‌ساز جاوا اسکریپت نینتندو نشان داده شده است.

از بوم های چند لایه برای صحنه های پیچیده استفاده کنید

همانطور که قبلا ذکر شد، ترسیم تصاویر بزرگ گران است و در صورت امکان باید از آن اجتناب کرد. علاوه بر استفاده از بوم دیگری برای رندر خارج از صفحه، همانطور که در بخش پیش رندر نشان داده شده است، می توانیم از بوم های لایه بندی شده روی هم استفاده کنیم. با استفاده از شفافیت در بوم پیش‌زمینه، می‌توانیم به GPU برای ترکیب آلفاها با هم در زمان رندر تکیه کنیم. می توانید این را به صورت زیر تنظیم کنید، با دو بوم کاملاً قرار گرفته یکی روی دیگری.

<canvas id="bg" width="640" height="480" style="position: absolute; z-index: 0">
</canvas>
<canvas id="fg" width="640" height="480" style="position: absolute; z-index: 1">
</canvas>

مزیت داشتن فقط یک بوم در اینجا این است که وقتی بوم پیش زمینه را می کشیم یا پاک می کنیم، هرگز پس زمینه را تغییر نمی دهیم. اگر بازی یا برنامه چندرسانه‌ای شما را می‌توان به پیش‌زمینه و پس‌زمینه تقسیم کرد، آن‌ها را روی بوم‌های جداگانه رندر کنید تا عملکرد قابل توجهی را افزایش دهید.

شما اغلب می توانید از درک ناقص انسان استفاده کنید و پس زمینه را فقط یک بار یا با سرعت کمتری نسبت به پیش زمینه (که احتمالاً بیشتر توجه کاربر شما را به خود جلب می کند) رندر کنید. برای مثال، می‌توانید هر بار که رندر می‌دهید، پیش‌زمینه را رندر کنید، اما پس‌زمینه را فقط در هر فریم نهم رندر کنید. همچنین توجه داشته باشید که اگر برنامه شما با این نوع ساختار بهتر کار کند، این رویکرد برای هر تعداد بوم کامپوزیت به خوبی تعمیم می یابد.

از shadowBlur اجتناب کنید

مانند بسیاری از محیط های گرافیکی دیگر، بوم HTML5 به توسعه دهندگان اجازه می دهد تا موارد اولیه را محو کنند، اما این عملیات می تواند بسیار گران باشد:

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = 'rgba(255, 0, 0, 0.5)';
context.fillRect(20, 20, 150, 100);

روش های مختلف برای پاک کردن بوم را بدانید

از آنجایی که بوم HTML5 یک الگوی ترسیم حالت فوری است، صحنه باید به صراحت در هر فریم دوباره ترسیم شود. به همین دلیل، پاک کردن بوم یک عملیات اساسی برای برنامه ها و بازی های بوم HTML5 است. همانطور که در بخش اجتناب از تغییرات حالت بوم ذکر شد، پاک کردن کل بوم اغلب نامطلوب است، اما اگر باید این کار را انجام دهید، دو گزینه وجود دارد: فراخوانی context.clearRect(0, 0, width, height) یا استفاده از هک مخصوص بوم. برای انجام این کار: canvas.width = canvas.width ;.در زمان نوشتن، clearRect معمولاً از نسخه بازنشانی عرضی بهتر عمل می کند، اما در برخی موارد استفاده از هک تنظیم مجدد canvas.width به طور قابل توجهی در Chrome 14 سریعتر است.

مراقب این نکته باشید، زیرا به شدت به اجرای بوم زیرین بستگی دارد و بسیار در معرض تغییر است. برای اطلاعات بیشتر، مقاله Simon Sarris در مورد پاک کردن بوم را ببینید.

از مختصات ممیز شناور اجتناب کنید

بوم HTML5 از رندر زیر پیکسل پشتیبانی می کند و راهی برای خاموش کردن آن وجود ندارد. اگر با مختصاتی رسم کنید که اعداد صحیح نیستند، به طور خودکار از anti-aliasing برای صاف کردن خطوط استفاده می کند. در اینجا جلوه بصری که از این مقاله عملکرد بوم زیر پیکسلی توسط سب لی-دلیل گرفته شده است:

زیر پیکسل

اگر sprite هموار شده اثر مورد نظر شما نیست، تبدیل مختصات شما به اعداد صحیح با استفاده از Math.floor یا Math.round ( jsperf ) می تواند بسیار سریعتر باشد:

برای تبدیل مختصات ممیز شناور به اعداد صحیح، می‌توانید از چندین تکنیک هوشمندانه استفاده کنید که کارآمدترین آنها شامل اضافه کردن یک نیمه به عدد مورد نظر و سپس انجام عملیات بیتی بر روی نتیجه برای حذف قسمت کسری است.

// With a bitwise or.
rounded = (0.5 + somenum) | 0;
// A double bitwise not.
rounded = ~~ (0.5 + somenum);
// Finally, a left bitwise shift.
rounded = (0.5 + somenum) << 0;

تفکیک عملکرد کامل در اینجا ( jsperf ) است.

توجه داشته باشید که این نوع بهینه سازی پس از تسریع در پیاده سازی بوم GPU که می تواند مختصات غیر صحیح را به سرعت ارائه کند، دیگر اهمیتی ندارد.

انیمیشن های خود را با requestAnimationFrame بهینه کنید

API نسبتا جدید requestAnimationFrame روش پیشنهادی برای پیاده سازی برنامه های کاربردی تعاملی در مرورگر است. به جای اینکه به مرورگر دستور دهید تا با نرخ تیک ثابت خاصی رندر شود، مودبانه از مرورگر می‌خواهید تا روال رندر شما را فراخوانی کند و زمانی که مرورگر در دسترس است با شما تماس گرفته شود. به عنوان یک اثر جانبی خوب، اگر صفحه در پیش زمینه نباشد، مرورگر آنقدر هوشمند است که نمی تواند رندر شود. درخواست پاسخ به تماس requestAnimationFrame نرخ بازگشت به تماس 60 فریم در ثانیه را هدف قرار می دهد، اما آن را تضمین نمی کند، بنابراین باید مدت زمان گذشته از آخرین رندر را پیگیری کنید. این می تواند چیزی شبیه به شکل زیر باشد:

var x = 100;
var y = 100;
var lastRender = Date.now();
function render() {
  var delta = Date.now() - lastRender;
  x += delta;
  y += delta;
  context.fillRect(x, y, W, H);
  requestAnimationFrame(render);
}
render();

توجه داشته باشید که این استفاده از requestAnimationFrame برای بوم و همچنین سایر فناوری‌های رندر مانند WebGL کاربرد دارد. در زمان نگارش این مطلب، این API فقط در کروم، سافاری و فایرفاکس موجود است، بنابراین باید از این شیم استفاده کنید.

اکثر پیاده سازی های بوم موبایل کند هستند

بیایید در مورد موبایل صحبت کنیم. متأسفانه در زمان نگارش این مقاله، تنها iOS 5.0 نسخه بتا که Safari 5.1 را اجرا می‌کند، دارای GPU تسریع‌شده در بوم موبایل است. بدون شتاب GPU، مرورگرهای تلفن همراه معمولاً پردازنده‌های قدرتمند کافی برای برنامه‌های مبتنی بر بوم مدرن ندارند. تعدادی از تست‌های JSPerf که در بالا توضیح داده شد، در مقایسه با دسکتاپ در تلفن همراه بدتر عمل می‌کنند و انواع برنامه‌های بین دستگاهی را که می‌توانید انتظار اجرای موفقیت‌آمیز آن‌ها را داشته باشید، بسیار محدود می‌کند.

نتیجه

برای جمع‌بندی، این مقاله مجموعه جامعی از تکنیک‌های بهینه‌سازی مفید را پوشش می‌دهد که به شما کمک می‌کند پروژه‌های مبتنی بر بوم HTML5 را توسعه دهید. اکنون که در اینجا چیز جدیدی یاد گرفتید، جلو بروید و خلاقیت های عالی خود را بهینه کنید. یا، اگر در حال حاضر بازی یا برنامه‌ای برای بهینه‌سازی ندارید، برای الهام گرفتن از Chrome Experiments و Creative JS دیدن کنید.

منابع

  • حالت فوری در مقابل حالت حفظ شده .
  • سایر مقالات بوم HTML5Rocks .
  • بخش Canvas Dive into HTML5.
  • JSPerf به توسعه دهندگان اجازه می دهد تا تست های عملکرد JS را ایجاد کنند.
  • Browserscope داده های عملکرد مرورگر را ذخیره می کند.
  • JSPerfView ، که تست های JSPerf را به صورت نمودار ارائه می کند.
  • پست وبلاگ سایمون در مورد پاک کردن بوم، و کتاب او، HTML5 Unleashed که شامل فصل هایی در مورد عملکرد بوم است.
  • پست وبلاگ سباستین در مورد عملکرد رندر زیر پیکسل.
  • صحبت بن در مورد بهینه سازی شبیه ساز JS NES.
  • نمایه‌ساز بوم جدید در ابزار توسعه کروم.