تسريع المخططات باستخدام OffscreenCanvas

يمكن أن يؤثر تقديم الرسم البياني بشكل خطير على المتصفح. خاصة عندما يتعلق الأمر بإخراج العديد من العناصر التي تمثل الرسوم البيانية في واجهة تطبيق معقد. يمكنك محاولة تحسين الوضع باستخدام الواجهة OffscreenCanvas، ومستوى الدعم الذي تنمو فيه المتصفحات تدريجيًا. يسمح ، باستخدام عامل ويب ، بتحويل مهام تكوين صورة معروضة في عنصر مرئي إليها <canvas>. المقالة ، التي ننشر ترجمتها اليوم ، مكرسة لاستخدام الواجهة . هنا سنتحدث عن سبب الحاجة إلى هذه الواجهة ، وعن ما يمكن توقعه حقًا من استخدامه ، وعن الصعوبات التي قد تنشأ عند العمل معها.



OffscreenCanvas

لماذا الاهتمام بـ OffscreenCanvas؟


قد يكون للرمز المتضمن في عرض الرسم البياني نفسه تعقيدًا حسابيًا مرتفعًا بما فيه الكفاية. في العديد من الأماكن ، يمكنك العثور على قصص تفصيلية حول كيفية إخراج رسوم متحركة سلسة ولضمان تفاعل المستخدم المريح مع الصفحة ، فمن الضروري أن يتم عرض إطار واحد في غضون حوالي 10 مللي ثانية ، مما يسمح لك بالوصول إلى 60 إطارًا في الثانية. إذا كانت الحسابات المطلوبة لإخراج إطار واحد تستغرق أكثر من 10 مللي ثانية ، فسيؤدي ذلك إلى "تباطؤ" ملحوظ في الصفحة.

عند عرض الرسوم البيانية ، يتفاقم الوضع من خلال ما يلي:

  • — - (SVG, canvas, WebGL) , , .
  • , , , , 10 . ( — ) .

هذه المشاكل هي مرشحات مثالية لمنهج التحسين الكلاسيكي يسمى فرق تسد. في هذه الحالة ، نحن نتحدث عن توزيع حمل الحوسبة عبر خيوط متعددة. ومع ذلك ، قبل ظهور الواجهة ، OffscreenCanvasكان يجب تنفيذ جميع تعليمات الكود في الخيط الرئيسي. الطريقة الوحيدة التي يمكن لهذا الرمز من خلالها استخدام واجهات برمجة التطبيقات التي يحتاجها.

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

تخصيصOffscreenCanvasيمنحنا آلية لنقل التحكم السطحي لعرض رسومات العناصر <canvas>في عامل الويب. هذه المواصفات مدعومة حاليًا من متصفحي Chrome و Edge (بعد ترحيل Edge إلى Chromium). من المتوقع أن OffscreenCanvasيظهر دعم Firefox في غضون ستة أشهر. إذا تحدثنا عن Safari ، فلا يزال من غير المعروف ما إذا كان دعم هذه التقنية مخططًا له في هذا المتصفح.

إخراج الرسم البياني باستخدام OffscreenCanvas



مثال على مخطط يعرض 100000 نقطة متعددة الألوان من

أجل تقييم أفضل للمزايا والعيوب المحتملةOffscreenCanvas، دعنا نلقي نظرة على مثال للمخطط "الثقيل" الموضح في الشكل السابق.

const offscreenCanvas = canvasContainer
  .querySelector('canvas')
  .transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);

أولاً ، نستعلم OffscreenCanvasالعنصر canvasباستخدام الطريقة الجديدة transferControlToOffscreen(). ثم نطلق على الطريقة worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas])، للقيام بذلك لإرسال رسالة إلى العامل تحتوي على رابط إلى offscreenCanvas. من المهم جدًا أن تستخدم هنا ، كحجة ثانية [offscreenCanvas]. هذا يسمح لك بجعل مالك هذا الكائن عاملًا ، مما سيسمح له بالإدارة بمفرده OffscreenCanvas.

canvasContainer.addEventListener('measure', ({ detail }) => {
  const { width, height } = detail;
  worker.postMessage({ width, height });
});
canvasContainer.requestRedraw();

وبالنظر إلى أن أحجام و OffscreenCanvasرثت أصلا من سمات widthو heightالعنصر canvas، فإنه تقع على عاتقنا مسؤولية الحفاظ على هذه القيم حتى الآن عند تغيير حجم عنصر canvas. هنا نستخدم الحدث measureمن d3fc-canvas، الذي سيتيح لنا الفرصة ، بالاتفاق مع requestAnimationFrame، لنقل المعلومات إلى العامل حول الأبعاد الجديدة للعنصر canvas.

لتبسيط المثال ، سنستخدم مكونات من مكتبة d3fc . هذه مجموعة من المكونات الإضافية التي تقوم إما بتجميع مكونات d3 أو تكمل وظائفها. يمكنك العمل OffscreenCanvasبدون مكونات d3fc. يمكن إجراء كل ما سيتم مناقشته باستخدام ميزات JavaScript القياسية الحصرية.

انتقل الآن إلى الرمز من الملف worker.js. في هذا المثال ، من أجل زيادة حقيقية في أداء العرض ، سنستخدم WebGL.

addEventListener('message', ({ data: { offscreenCanvas, width, height } }) => {
    if (offscreenCanvas != null) {
        const gl = offscreenCanvas.getContext('webgl');
        series.context(gl);
        series(data);
    }

    if (width != null && height != null) {
        const gl = series.context();
        gl.canvas.width = width;
        gl.canvas.height = height;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    }
});

عندما نتلقى رسالة تحتوي على خاصية canvas، نفترض أن الرسالة جاءت من سلسلة المحادثات الرئيسية. بعد ذلك ، نحصل على canvasالسياق من العنصر webglونمرره إلى المكون series. ثم نسمي المكون series، ونمرره البيانات ( data) ، التي نريد تقديمها باستخدامه (أدناه سنتحدث عن مصدر المتغيرات المقابلة).

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

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

const randomNormal = d3.randomNormal(0, 1);
const randomLogNormal = d3.randomLogNormal();

const data = Array.from({ length: 1e5 }, () => ({
    x: randomNormal(),
    y: randomNormal(),
    size: randomLogNormal() * 10
}));

const xScale = d3.scaleLinear().domain([-5, 5]);

const yScale = d3.scaleLinear().domain([-5, 5]);

أولاً ، ننشئ مجموعة بيانات تحتوي على إحداثيات النقاط الموزعة عشوائيًا حول الأصل x / y ، ونضبط القياس. لا نقوم بتعيين نطاق قيم x و y باستخدام الطريقة range، حيث يتم عرض سلسلة WebGL بالحجم الكامل للعنصر canvas (-1 -> +1في الإحداثيات المعيارية للجهاز).

const series = fc
    .seriesWebglPoint()
    .xScale(xScale)
    .yScale(yScale)
    .crossValue(d => d.x)
    .mainValue(d => d.y)
    .size(d => d.size)
    .equals((previousData, data) => previousData.length > 0);

بعد ذلك ، قمنا بإعداد سلسلة من النقاط باستخدام xScaleو yScale. ثم نقوم بتكوين وسائل الوصول المناسبة التي تسمح لك بقراءة البيانات.

بالإضافة إلى ذلك ، نحدد وظيفة التحقق من المساواة الخاصة بنا ، والتي تم تصميمها للتأكد من أن المكون لا يرسل dataوحدات معالجة الرسومات في كل عرض. يجب أن نعبر عن هذا صراحة ، لأنه حتى لو علمنا أننا لن نقوم بتعديل هذه البيانات ، لا يمكن للمكون أن يعرف عنها بدون إجراء فحص "قذر" كثيف الموارد.

const colorScale = d3.scaleOrdinal(d3.schemeAccent);

const webglColor = color => {
    const { r, g, b, opacity } = d3.color(color).rgb();
    return [r / 255, g / 255, b / 255, opacity];
};

const fillColor = fc
    .webglFillColor()
    .value((d, i) => webglColor(colorScale(i)))
    .data(data);

series.decorate(program => {
    fillColor(program);
});

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

function render() {
    const ease = 5 * (0.51 + 0.49 * Math.sin(Date.now() / 1e3));
    xScale.domain([-ease, ease]);
    yScale.domain([-ease, ease]);
    series(data);
    requestAnimationFrame(render);
}

الآن بعد أن أصبحت النقاط ملونة ، كل ما تبقى هو تحريك الرسم البياني. ونتيجة لذلك ، سنحتاج إلى استدعاء أسلوب ثابت render(). هذا ، بالإضافة إلى ذلك ، سيخلق بعض الحمل على النظام. نحن محاكاة الزيادة وحجم الانخفاض الرسم البياني باستخدام requestAnimationFrameلتعديل خصائص xScale.domainو yScale.domainكل إطار. هنا ، يتم تطبيق القيم التي تعتمد على الوقت وحسابها بحيث يتغير مقياس الرسم البياني بسلاسة. بالإضافة إلى ذلك ، نقوم بتعديل رأس الرسالة بحيث يتم استدعاء طريقة لبدء دورة العرض render()، بحيث لا نضطر إلى الاتصال مباشرة series(data).

importScripts(
    './node_modules/d3-array/dist/d3-array.js',
    './node_modules/d3-collection/dist/d3-collection.js',
    './node_modules/d3-color/dist/d3-color.js',
    './node_modules/d3-interpolate/dist/d3-interpolate.js',
    './node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.js',
    './node_modules/d3-random/dist/d3-random.js',
    './node_modules/d3-scale/dist/d3-scale.js',
    './node_modules/d3-shape/dist/d3-shape.js',
    './node_modules/d3fc-extent/build/d3fc-extent.js',
    './node_modules/d3fc-random-data/build/d3fc-random-data.js',
    './node_modules/d3fc-rebind/build/d3fc-rebind.js',
    './node_modules/d3fc-series/build/d3fc-series.js',
    './node_modules/d3fc-webgl/build/d3fc-webgl.js'
);

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

→ يمكن العثور على الرمز الكامل لهذا المثال على GitHub

الشاشة والأداء


تعمل الرسوم المتحركة في مشروعنا بفضل استخدام requestAnimationFrameعامل. يسمح هذا للعامل بعرض الصفحات حتى عندما يكون الخيط الرئيسي مشغولًا بأشياء أخرى. إذا نظرت إلى صفحة المشروع الموضحة في القسم السابق ونقرت على الزر Stop main thread، يمكنك الانتباه إلى حقيقة أنه عندما يتم عرض مربع الرسالة ، يتوقف تحديث معلومات الطابع الزمني. تم حظر سلسلة الرسائل الرئيسية ، لكن الرسوم المتحركة للرسم البياني لا تتوقف.


نافذة المشروع

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

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

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

يمكنك استخدام واجهة MessageChannel القياسية لإنشاء قنوات رسائل منفصلة بين سلسلة الرسائل الرئيسية وسلسلة الرسائل التابعة .. يمكن استخدام كل من هذه القنوات لفئة معينة من الرسائل ، مما يسهل معالجة الرسائل. كبديل للآليات القياسية ، يمكن أن تعمل مكتبات الطرف الثالث مثل Comlink . تخفي هذه المكتبة التفاصيل ذات المستوى المنخفض خلف واجهة عالية المستوى باستخدام كائنات الوكيل .

ميزة أخرى مثيرة للاهتمام في مثالنا ، والتي أصبحت مرئية بوضوح في وقت لاحق ، هي حقيقة أنها تأخذ في الاعتبار حقيقة أن موارد GPU ، مثل موارد النظام الأخرى ، أبعد ما تكون عن اللانهائية. تسمح ترجمة التقديم إلى عامل لمؤشر الترابط الرئيسي بحل مشاكل أخرى. لكن المتصفح ، على أي حال ، يحتاج إلى استخدام GPU لتقديم DOM وما تم إنشاؤه بالوسائل OffscreenCanvas.

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

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

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

ملخص


OffscreenCanvasتتيح الواجهة للمطورين فرصة تحسين أداء عرض الرسم البياني ، ولكن استخدامه يتطلب نهجًا مدروسًا. باستخدامه ، تحتاج إلى مراعاة ما يلي:

  • (, , ) - .

    • , . , , , postMessage.
  • , (, SVG/HTML- <canvas>), , , .

    • , , , , , , , , .
  • , GPU, OffscreenCanvas .

    • OffscreenCanvas, GPU-, , . , , .

هنا رمز مثال ، وإصداره يعمل.

القراء الأعزاء! هل استخدمت OffscreenCanvas لتسريع عرض الرسومات على صفحات الويب؟


All Articles