การปรับปรุงประสิทธิภาพการทำงานของ HTML5 Canvas

เกริ่นนำ

Canvas ของ HTML5 ซึ่งเริ่มเป็นการทดสอบจาก Apple เป็นมาตรฐานที่รองรับอย่างกว้างขวางมากที่สุดสำหรับกราฟิกในโหมดทันทีแบบ 2 มิติบนเว็บ ปัจจุบันนักพัฒนาซอฟต์แวร์จำนวนมากใช้แอปพลิเคชันนี้ในโปรเจ็กต์มัลติมีเดีย การแสดงภาพข้อมูล และเกมที่หลากหลาย อย่างไรก็ตาม ในขณะที่แอปพลิเคชันมีความซับซ้อนมากขึ้นเรื่อยๆ นักพัฒนาแอปก็ฝ่าฟันกำแพงประสิทธิภาพออกโดยไม่ได้ตั้งใจ การเพิ่มประสิทธิภาพ Canvas นั้นมีอยู่มากมาย บทความนี้มีจุดประสงค์เพื่อรวบรวมเนื้อหาบางส่วนไว้ในแหล่งข้อมูลที่เข้าใจได้ง่ายขึ้นสำหรับนักพัฒนาซอฟต์แวร์ บทความนี้รวมถึงการเพิ่มประสิทธิภาพพื้นฐานที่มีผลกับสภาพแวดล้อมกราฟิกคอมพิวเตอร์ทั้งหมด ตลอดจนเทคนิคเฉพาะสำหรับ Canvas ซึ่งอาจมีการเปลี่ยนแปลงเมื่อการใช้งาน Canvas ดีขึ้น โดยเฉพาะอย่างยิ่ง เมื่อผู้ให้บริการเบราว์เซอร์ใช้การเร่ง GPU ของ Canvas เทคนิคด้านประสิทธิภาพบางส่วนที่กล่าวถึงไปแล้วจะมีประสิทธิภาพน้อยลง เราจะแจ้งให้ทราบตามความเหมาะสม โปรดทราบว่าบทความนี้ไม่ได้กล่าวถึงการใช้งานแคนวาส HTML5 หากต้องการทำเช่นนั้น โปรดดูบทความที่เกี่ยวข้องกับแคนวาสเหล่านี้ใน HTML5Rocks บทในส่วนเว็บไซต์ "เจาะลึก HTML5" นี้หรือ MDN Canvas บทแนะนำ

การทดสอบประสิทธิภาพ

เพื่อรับมือกับโลกของ Canvas ของ HTML5 ที่เปลี่ยนแปลงไปอย่างรวดเร็ว การทดสอบ JSPerf (jsperf.com) จะยืนยันว่าการเพิ่มประสิทธิภาพทั้งหมดที่เสนอยังคงใช้งานได้ JSPerf คือเว็บแอปพลิเคชันที่ช่วยให้นักพัฒนาซอฟต์แวร์ เขียนการทดสอบประสิทธิภาพ JavaScript ได้ การทดสอบแต่ละครั้งจะมุ่งเน้นไปยังผลลัพธ์ที่คุณพยายามทำให้สำเร็จ (เช่น การล้างพื้นที่เก็บข้อมูล) และมีหลายแนวทางที่ทำให้ได้ผลลัพธ์เดียวกัน JSPerf จะเรียกใช้แต่ละวิธีพร้อมกันให้มากที่สุดเท่าที่จะเป็นไปได้ภายในช่วงเวลาสั้นๆ และให้จำนวนการดำเนินการซ้ำอย่างมีนัยสำคัญทางสถิติต่อวินาที คะแนนสูงย่อมดีกว่าเสมอ ผู้เข้าชมหน้าทดสอบประสิทธิภาพ JSPerf จะทำการทดสอบในเบราว์เซอร์ของตน และให้ JSPerf จัดเก็บผลการทดสอบมาตรฐานใน Browserscope (browserscope.org) ได้ เนื่องจากเทคนิคการเพิ่มประสิทธิภาพในบทความนี้มีการสำรองข้อมูลโดยผลลัพธ์ JSPerf คุณจึงกลับมาดูข้อมูลล่าสุดได้ว่ายังมีการใช้เทคนิคอยู่ไหม เราได้เขียนแอปพลิเคชันผู้ช่วยขนาดเล็กที่แสดงผลลัพธ์เป็นกราฟที่ฝังอยู่ตลอดทั้งบทความนี้

ผลลัพธ์ด้านประสิทธิภาพทั้งหมดในบทความนี้ขึ้นอยู่กับเวอร์ชันของเบราว์เซอร์ ซึ่งถือว่าเป็นข้อจำกัด เนื่องจากเราไม่รู้ว่าเบราว์เซอร์ใช้ระบบปฏิบัติการใด หรือที่สำคัญกว่านั้นคือ Canvas ของ HTML5 มีการเร่งฮาร์ดแวร์เมื่อทำการทดสอบประสิทธิภาพหรือไม่ คุณสามารถดูได้ว่าแคนวาส HTML5 ของ Chrome มีการเร่งฮาร์ดแวร์หรือไม่โดยไปที่ about:gpu ในแถบที่อยู่

แสดงผลล่วงหน้าไปยังผืนผ้าใบนอกหน้าจอ

หากคุณทำการวาดรูปแบบพื้นฐานที่คล้ายกันไปยังหน้าจอในหลายเฟรมเหมือนๆ กับขณะเขียนเกม คุณสามารถเพิ่มประสิทธิภาพการทำงานได้อย่างมากจากการแสดงผลฉากใหญ่ๆ ล่วงหน้า การแสดงผลล่วงหน้าคือการใช้แคนวาส (หรือผืนผ้าใบ) นอกหน้าจอแยกต่างหากเพื่อแสดงผลรูปภาพชั่วคราว และแสดงผลผืนผ้าใบนอกหน้าจอกลับไปยังรูปภาพที่มองเห็นได้ ตัวอย่างเช่น สมมติว่าคุณกำลังวาด Mario ใหม่ให้ทำงานที่ 60 เฟรมต่อวินาที คุณอาจวาดหมวก หนวด และตัว "M" บนแต่ละเฟรม หรือให้ Mario แสดงผลล่วงหน้าก่อนแสดงภาพเคลื่อนไหว ไม่มีการแสดงผลล่วงหน้า:

// 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;

รวมการโทรผ่าน Canvas ไว้ด้วยกัน

เนื่องจากการวาดเป็นการดำเนินการที่มีราคาสูง การโหลดเครื่องที่สถานะภาพวาดด้วยชุดคำสั่งยาวๆ จึงเป็นการมีประสิทธิภาพมากกว่า แล้วถ่ายโอนข้อมูลทั้งหมดลงในบัฟเฟอร์วิดีโอ

ตัวอย่างเช่น เวลาวาดหลายเส้น การสร้างเส้นทางเดียวที่มีเส้นทั้งหมดในเส้นและวาดบรรทัดเดียวจะมีประสิทธิภาพมากกว่า ใช้คำอื่นๆ แทนการลากเส้นแยกกัน

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)

หลีกเลี่ยงการเปลี่ยนสถานะ Canvas ที่ไม่จำเป็น

องค์ประกอบ Canvas ของ 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);

หากคุณคุ้นเคยกับคอมพิวเตอร์กราฟิก คุณอาจรู้จักเทคนิคนี้ในชื่อ "วาดพื้นที่ใหม่" ซึ่งจะบันทึกกรอบล้อมรอบที่แสดงผลก่อนหน้านี้ไว้ แล้วล้างออกจากการแสดงผลแต่ละครั้ง เทคนิคนี้ยังสามารถใช้ได้กับบริบทการแสดงผลแบบพิกเซลด้วย ดังที่เห็นได้จากการบรรยายของโปรแกรมจำลอง Nintendo JavaScript นี้

ใช้ผืนผ้าใบหลายชั้นสำหรับฉากที่ซับซ้อน

ดังที่กล่าวไปแล้ว การวาดรูปภาพขนาดใหญ่มีค่าใช้จ่ายสูงและควรหลีกเลี่ยงหากทำได้ นอกเหนือจากการใช้ผืนผ้าใบอีกชิ้นเพื่อแสดงผลนอกหน้าจอดังที่แสดงในส่วนการแสดงผลล่วงหน้าแล้ว เรายังใช้ผืนผ้าใบที่วางซ้อนกันได้ การใช้ความโปร่งใสใน Canvas เบื้องหน้าทำให้เราใช้ GPU ในการประกอบตัวอักษรเข้าด้วยกันในเวลาแสดงผลได้ คุณอาจตั้งค่าดังนี้ โดยให้ผืนผ้าใบที่มีตำแหน่งแบบสัมบูรณ์ 2 อันโดยให้ข้อความหนึ่งอยู่ด้านบน

<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>

ข้อดีของการมีแคนวาสเพียงหน้าเดียวคือ เมื่อเราวาดหรือล้างแคนวาสที่เบื้องหน้า เราจะไม่แก้ไขพื้นหลังเลย หากคุณแบ่งแอปเกมหรือแอปมัลติมีเดียออกเป็นเบื้องหน้าและเบื้องหลังได้ ให้พิจารณาแสดงภาพเหล่านี้บนผืนผ้าใบแยกต่างหากเพื่อเพิ่มประสิทธิภาพอย่างมาก

คุณมักใช้ประโยชน์จากการรับรู้ที่ไม่สมบูรณ์แบบของมนุษย์และแสดงผลพื้นหลังเพียงครั้งเดียวหรือแสดงผลด้วยความเร็วที่ต่ำลงเมื่อเทียบกับพื้นหน้า (ซึ่งมีแนวโน้มที่จะดึงความสนใจของผู้ใช้ได้เกือบทั้งหมด) เช่น คุณสามารถแสดงผลพื้นหน้าทุกครั้งที่แสดงผล แต่แสดงผลพื้นหลังทุกเฟรมที่ N เท่านั้น นอกจากนี้ โปรดทราบว่าวิธีการนี้เหมาะสำหรับแคนวาสของคอมโพเนนต์กี่รายการก็ได้หากแอปพลิเคชันของคุณทำงานได้ดีขึ้นกับโครงสร้างประเภทนี้

หลีกเลี่ยงแสงเงา

เช่นเดียวกับสภาพแวดล้อมกราฟิกอื่นๆ Canvas ของ HTML5 ช่วยให้นักพัฒนาซอฟต์แวร์สามารถเบลอแบบพื้นฐาน แต่การดำเนินการนี้อาจมีค่าใช้จ่ายสูง

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

รู้วิธีล้างผืนผ้าใบ

เนื่องจาก Canvas ของ HTML5 เป็นกระบวนทัศน์การวาดในโหมดทันที จึงต้องวาดฉากใหม่อย่างชัดเจนในแต่ละเฟรม ด้วยเหตุนี้ การล้าง Canvas จึงเป็นการดำเนินการที่สำคัญพื้นฐานสำหรับแอปและเกม แคนวาส HTML5 ดังที่กล่าวไว้ในส่วนหลีกเลี่ยงการเปลี่ยนสถานะ Canvas การล้าง Canvas ทั้งหมดมักจะไม่เป็นที่ต้องการ แต่หากคุณต้องดำเนินการนี้ จะมี 2 ตัวเลือก ได้แก่ การเรียก context.clearRect(0, 0, width, height) หรือการแฮ็กเฉพาะ Canvas เพื่อดำเนินการ ดังนี้ canvas.width = canvas.width; ขณะที่เขียน โดยทั่วไป clearRect จะมีประสิทธิภาพสูงกว่าเวอร์ชันรีเซ็ตความกว้าง แต่ในบางกรณีการใช้ canvas.width การรีเซ็ตจะรวดเร็วขึ้นมากใน Chrome 14

โปรดใช้ความระมัดระวังกับเคล็ดลับนี้ เนื่องจากขึ้นอยู่กับการใช้งาน Canvas ที่เกี่ยวข้องเป็นหลักและอาจมีการเปลี่ยนแปลงได้อย่างมาก ดูข้อมูลเพิ่มเติมได้ที่บทความของ Simon Sarris เกี่ยวกับการล้างผืนผ้าใบ

หลีกเลี่ยงพิกัดจุดลอยตัว

Canvas ของ HTML5 รองรับการแสดงผลพิกเซลย่อยและจะปิดไม่ได้ หากคุณวาดด้วยพิกัดที่ไม่ใช่จำนวนเต็ม โปรแกรมใช้การลบรอยหยักโดยอัตโนมัติเพื่อพยายามทำให้เส้นเรียบเนียน นี่คือเอฟเฟกต์ภาพที่นำมาจากบทความเรื่องประสิทธิภาพของ Canvas ระดับย่อยโดย Seb Lee-Delisle

พิกเซลย่อย

หากสไปรท์ที่เรียบเนียนไม่ใช่เอฟเฟ็กต์ที่คุณต้องการ การแปลงพิกัดเป็นจำนวนเต็มโดยใช้ Math.floor หรือ Math.round (jsperf) อาจเร็วกว่ามาก)

หากต้องการแปลงพิกัดจุดลอยตัวเป็นจำนวนเต็ม คุณสามารถใช้เทคนิคที่ชาญฉลาดหลายอย่าง ซึ่งมีประสิทธิภาพมากที่สุดโดยการนำ 1 ครึ่งไปบวกกับจำนวนเป้าหมาย จากนั้นจึงดำเนินการบิตไวส์กับผลลัพธ์เพื่อนำส่วนที่เป็นเศษส่วนออก

// 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)

โปรดทราบว่าการเพิ่มประสิทธิภาพประเภทนี้ไม่ควรเกิดขึ้นอีกเมื่อการใช้งาน Canvas มีการเร่ง GPU ซึ่งจะแสดงพิกัดที่ไม่ใช่จำนวนเต็มได้อย่างรวดเร็ว

เพิ่มประสิทธิภาพภาพเคลื่อนไหวด้วย requestAnimationFrame

requestAnimationFrame API ที่ค่อนข้างใหม่เป็นวิธีที่แนะนำในการนำแอปพลิเคชันแบบอินเทอร์แอกทีฟไปใช้ในเบราว์เซอร์ แทนที่จะสั่งเบราว์เซอร์ให้แสดงผลที่อัตราคงที่ที่เจาะจง คุณขอให้เบราว์เซอร์เรียกกิจวัตรการแสดงผลและเรียกเมื่อเบราว์เซอร์พร้อมใช้งาน ผลข้างเคียงที่ดีคือ หากหน้าเว็บไม่ได้อยู่เบื้องหน้า หมายความว่าเบราว์เซอร์ฉลาดพอที่จะไม่แสดงผล โค้ดเรียกกลับ requestAnimationFrame มุ่งเป้าไปที่อัตราโค้ดเรียกกลับที่ 60 FPS แต่ไม่รับประกัน คุณจึงต้องติดตามระยะเวลาที่ผ่านไปนับตั้งแต่การแสดงผลครั้งล่าสุด ซึ่งจะมีลักษณะดังนี้

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 นี้มีผลกับ Canvas รวมถึงเทคโนโลยีการแสดงผลอื่นๆ เช่น WebGL ในขณะที่เขียน API นี้พร้อมใช้งานใน Chrome, Safari และ Firefox เท่านั้น คุณจึงควรใช้ shim นี้

การใช้งาน Canvas บนอุปกรณ์เคลื่อนที่ส่วนใหญ่จะช้า

มาพูดถึงอุปกรณ์เคลื่อนที่กัน น่าเสียดายที่เวลาที่เขียนบทความ มีเพียง iOS 5.0 รุ่นเบต้าที่ใช้ Safari 5.1 เท่านั้นที่มีการใช้งาน Canvas Accelerated Mobile ที่ใช้ GPU เป็นตัวเร่ง หากไม่เร่ง GPU เบราว์เซอร์ในอุปกรณ์เคลื่อนที่จะมี CPU ที่มีประสิทธิภาพไม่เพียงพอสำหรับแอปพลิเคชันที่ใช้ Canvas สมัยใหม่ การทดสอบ JSPerf จำนวนหนึ่งที่อธิบายข้างต้นกลับมีประสิทธิภาพในอันดับที่แย่ลงบนอุปกรณ์เคลื่อนที่เมื่อเทียบกับเดสก์ท็อป ซึ่งทำให้มีการจำกัดประเภทแอปข้ามอุปกรณ์ที่จะเรียกใช้ได้สำเร็จอย่างมาก

บทสรุป

โดยสรุปแล้ว บทความนี้ครอบคลุมชุดเทคนิคการเพิ่มประสิทธิภาพที่มีประโยชน์ซึ่งจะช่วยคุณพัฒนาโปรเจ็กต์ที่ใช้ผืนผ้าใบที่มีประสิทธิภาพสูงสุด ตอนนี้เมื่อคุณได้เรียนรู้สิ่งใหม่ๆ ที่นี่ แล้วต่อไปก็เพิ่มประสิทธิภาพผลงานอันยอดเยี่ยมของคุณ หรือหากคุณยังไม่มีเกมหรือแอปพลิเคชันสำหรับเพิ่มประสิทธิภาพ โปรดดูการทดสอบของ Chrome และ Creative JS เพื่อเป็นแรงบันดาลใจ

รายการอ้างอิง