تحسين أداء التشفير: تشفير سريع في المتصفح
حسّن أداء التشفير للنقل الكبير. التشفير المتدفق وWeb Workers وتقنيات المعالجة المجزأة.
تشفير ملف بحجم 5 جيجابايت في المتصفح دون تجميد واجهة المستخدم يستلزم مجموعة محددة من التقنيات: Web Crypto API للحصول على تسريع AES-256-GCM عبر الأجهزة (1-2 جيجابايت/ثانية مع AES-NI)، ومعالجة مجزّأة في كتل بحجم 1-4 ميجابايت للحفاظ على الذاكرة محدودة، وWeb Workers للإبقاء على الخيط الرئيسي مستجيباً، وقراءة متدفقة عبر File.stream() بدلاً من FileReader.readAsArrayBuffer()، وإدارة دقيقة للـ nonces حتى يمكن تشفير الأجزاء بالتوازي. تعمل مكتبات التشفير المكتوبة بـ JavaScript خالصة بسرعة أبطأ بـ 10-20 مرة من Web Crypto، وينبغي الاحتفاظ بها للمكوّنات التي لا يعرضها المتصفح أصلاً. إليك كيفية الوصول إلى إنتاجية تتجاوز المئات من ميجابايت في الثانية على أجهزة المستخدمين الفعلية.
القياس أولاً قبل التحسين
قبل التحسين، قِس. على MacBook Air M2 الإصدار 2024 في Chrome 120، يعمل تشفير مخزن مؤقت بحجم 1 جيجابايت بـ AES-256-GCM عبر Web Crypto بسرعة ~1.7 جيجابايت/ثانية. العملية ذاتها على هاتف متوسط المستوى (Pixel 7) تعمل بسرعة ~600 ميجابايت/ثانية. حاسوب Intel قديم من 2015 مع AES برمجياً فقط يصل إلى ~250 ميجابايت/ثانية.
ما يخبرك به هذا: AES-GCM عبر Web Crypto ليس عنق الزجاجة في أغلب تدفقات نقل الملفات المشفرة. عنق الزجاجة عادةً هو قراءة الملف، وترتيب JavaScript بين مخازن ArrayBuffers، أو رفع الشبكة. حسّن هذه أولاً.
const blob = new Uint8Array(1024 * 1024 * 100); // 100 ميجابايت
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 }, true, ["encrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const start = performance.now();
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, blob);
console.log(`${(100 / (performance.now() - start) * 1000).toFixed(0)} MB/s`);
شغّل الاختبار على Chrome وFirefox وSafari. Chrome على Apple Silicon سيكون الأسرع؛ Firefox أبطأ قليلاً؛ Safari على Intel أبطأ بشكل طفيف. الأجهزة المحمولة تعمل بنحو 30-50% من سرعة سطح المكتب.
استخدم Web Crypto لا مكتبات JavaScript
بالنسبة لـ AES-GCM وAES-CBC وPBKDF2 وHMAC وRSA وECDH وECDSA وSHA-256/384/512، تستخدم Web Crypto API الأصلية في المتصفح التسريع العتادي حيثما توفّر (AES-NI على x86، وامتدادات تشفير ARMv8 على الأجهزة المحمولة). لا تستطيع مكتبات JavaScript مثل @noble/ciphers أو crypto-js الخالصة الوصول إلى هذه التعليمات وتعمل في وضع المترجم فقط.
نسبة السرعة النموذجية لـ AES-256-GCM على مُدخل بحجم 1 ميجابايت:
- Web Crypto (تسريع عتادي): 1-2 جيجابايت/ثانية
- libsodium.js WASM: 400-800 ميجابايت/ثانية
- @noble/ciphers (JS خالصة): 100-200 ميجابايت/ثانية
- crypto-js (JS خالصة): 30-80 ميجابايت/ثانية
لملف بحجم 5 جيجابايت، الفارق بين Web Crypto والـ JS الخالصة هو ~3 ثوانٍ مقابل ~50 ثانية — فارق مرئي للمستخدم. افضّل دائماً Web Crypto لما تدعمه. استخدم مكتبات WASM (libsodium.js، argon2-browser) فقط للخوارزميات التي لا تمتلكها Web Crypto (ChaCha20-Poly1305، Argon2id، X25519 على المتصفحات القديمة).
المعالجة المجزّأة للملفات الكبيرة
الملفات التي تتجاوز ~500 ميجابايت لا تناسب مخزن ArrayBuffer واحداً على معظم الأجهزة. قسّمها إلى أجزاء بحجم 1-4 ميجابايت وشفّر كلاً منها.
const CHUNK_SIZE = 4 * 1024 * 1024; // 4 ميجابايت
async function encryptLargeFile(file, key) {
const chunks = [];
let chunkIndex = 0;
for (let offset = 0; offset < file.size; offset += CHUNK_SIZE) {
const chunk = await file.slice(offset, offset + CHUNK_SIZE).arrayBuffer();
const iv = new Uint8Array(12);
new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, chunk
);
chunks.push({ iv, ct: new Uint8Array(ct) });
}
return chunks;
}
لماذا 4 ميجابايت؟ الأجزاء الصغيرة (مثلاً 64 كيلوبايت) تُنشئ تكلفة ثابتة لكل استدعاء من Web Crypto API تهيمن على المُدخلات الصغيرة. الأجزاء الكبيرة (مثلاً 64 ميجابايت) لا تناسب ذاكرة التخزين المؤقت L2/L3 وتولّد ضغطاً أكبر على الذاكرة. يميل 1-4 ميجابايت إلى أن يكون النقطة المثلى على سطح المكتب والأجهزة المحمولة على حدٍّ سواء.
Web Workers للحفاظ على استجابة واجهة المستخدم
يحجب التشفير على الخيط الرئيسي عمليات الرسم والإدخال. لأي عمل يستغرق أكثر من ~500 مللي ثانية، انقله إلى Web Worker.
// worker.js
self.onmessage = async (e) => {
const { chunk, key, iv } = e.data;
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, chunk
);
self.postMessage(ciphertext, [ciphertext]);
};
// الخيط الرئيسي
const worker = new Worker('/worker.js');
worker.postMessage({ chunk, key, iv }, [chunk]);
worker.onmessage = (e) => { /* معالجة النص المشفر */ };
مرّر مخازن ArrayBuffers مع الوسيط الثاني لـ postMessage — هذا ينقل الملكية (نسخ صفري) بدلاً من الاستنساخ. بدون النقل، يُضيف استنساخ جزء بحجم 4 ميجابايت ~20 مللي ثانية تكلفة لكل جزء.
Web Crypto API متاحة في Workers، لذا يعمل التشفير الفعلي هناك بأداء مطابق للخيط الرئيسي.
تشفير الأجزاء بالتوازي
تشفير AES-GCM لكل جزء مستقل طالما لا تتصادم الـ nonces. اشتق الـ nonces بشكل حتمي من فهرس الجزء ويمكنك تشفير الأجزاء بالتوازي عبر عدة Workers. تبدأ العوائد المتناقصة بعد ~4 عمال على أجهزة نموذجية لأن Web Crypto سريعة لدرجة أن عنق الزجاجة ينتقل إلى قراءة الملف وتمرير الرسائل بين الخيوط. قِس قبل الالتزام بالتعقيد؛ أحياناً يكون التشفير التسلسلي بعامل واحد بنفس سرعة التشفير المتوازي بعمال متعددين بسبب التكلفة الثابتة.
البث عبر Readable Streams
للملفات الكبيرة حقاً (أكثر من 20 جيجابايت)، تجنّب تحميل حتى الأجزاء في الذاكرة دفعة واحدة. استخدم File.stream():
const reader = file.stream().getReader();
const writer = uploadStream.getWriter();
let chunkIndex = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const iv = new Uint8Array(12);
new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, value
);
await writer.write(new Uint8Array(ct));
}
await writer.close();
يُبقي هذا استخدام الذاكرة محدوداً بجزء واحد في كل مرة. يقرأ المتصفح من القرص، يشفّر، يكتب في تدفق الرفع، ثم يقرأ الجزء التالي. يبقى استخدام الذاكرة تحت 10 ميجابايت بغض النظر عن حجم الملف.
التزامن في الرفع
يعمل التشفير بالتوازي مع الرفع لا بشكل تسلسلي. بينما يُرفع الجزء رقم N، يُشفَّر الجزء رقم N+1. استخدم خط أنابيب:
const MAX_IN_FLIGHT = 4;
// المشفِّر يُنتج، والرافع يستهلك
async function pipeline() {
for (let i = 0; i < MAX_IN_FLIGHT; i++) startEncrypt(i);
// مع اكتمال كل جزء، ابدأ الرفع وأضف التشفير التالي للانتظار
}
يجعل البدء البطيء لـ TCP وتكلفة مصافحة TLS الرفع بطيئاً في البداية. الحفاظ على 4-8 رفعات متزامنة لأصل واحد يُبقي القناة مليئة دون تجاوز حدود المتصفح (6 اتصالات لكل أصل في Chrome/Firefox).
WebAssembly للمكوّنات الغائبة
لاشتقاق المفاتيح عبر Argon2id أو ChaCha20-Poly1305، لا يوجد دعم أصلي في Web Crypto. تسدّ مكتبات WASM هذه الفجوة:
- libsodium.js توفّر Argon2id وXChaCha20-Poly1305 و
crypto_secretstream - argon2-browser توفّر Argon2 فقط لكن بحجم أصغر
- @noble/hashes توفّر Argon2 بـ JS خالصة (أبطأ) بحجم حزمة صغير جداً
تُحقق إصدارات WASM 60-80% من السرعة الأصلية لمعظم أعباء التشفير. اشتقاق Argon2id بمدة 1-2 ثانية عند الرفع والتنزيل مقبول؛ أما 5 ثوانٍ بـ JS خالصة فليس مقبولاً.
حمّل WASM بشكل ديناميكي حتى لا يعيق عرض الصفحة الأولي:
const sodium = await import('libsodium-wrappers');
await sodium.ready;
الإبلاغ عن التقدم
عمليات التشفير الكبيرة تحتاج تغذية راجعة على التقدم وإلا يظن المستخدم أن التطبيق تجمّد. احسب البايتات المشفرة وأرسل أحداث التقدم:
let processed = 0;
for await (const chunk of chunks) {
await encryptChunk(chunk);
processed += chunk.size;
onProgress({ done: processed, total: file.size, pct: processed / file.size });
}
قيّد تحديثات واجهة المستخدم بـ ~10 مرات في الثانية عبر requestAnimationFrame أو فحص بسيط للطابع الزمني؛ التحديثات الأكثر تكراراً تُضيّع دورات على عمليات إعادة رسم لا يستطيع الإنسان إدراكها.
سقف الذاكرة وضغط جمع القمامة
كل مخزن ArrayBuffer يبقى حتى لا تعود تستخدمه أي إشارة. الاحتفاظ بـ 20 جزءاً مشفراً بحجم 4 ميجابايت يعني ~80 ميجابايت من الذاكرة المحجوزة. على متصفحات الأجهزة المحمولة ذات الحدود الضيقة (iOS Safari يحدّ كل علامة تبويب بـ ~200-400 ميجابايت)، هذا مهم.
أطلق المراجع فوراً:
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
chunks[i] = null; // دع جمع القمامة يستردّها
const ct = await encrypt(chunk);
await upload(ct);
}
تجنّب الاحتفاظ بمصفوفة كاملة من الأجزاء المشفرة في الذاكرة؛ بثّها إلى الرافع وأطلق المراجع أثناء التقدم.
الأهداف الواقعية
لنقل ملف بحجم 1 جيجابايت مشفّراً في علامة تبويب متصفح: وقت التشفير 1-3 ثوانٍ (مُسرَّع عتادياً)، وقت الرفع 30 ثانية على اتصال 300 ميجابت/ثانية، الإجمالي حوالي 35 ثانية (مقيّد بالشبكة في الغالب)، ذاكرة تحت 50 ميجابايت في الذروة مع البث الصحيح، وواجهة مستخدم مستجيبة طوال الوقت (الخيط الرئيسي لا يُحجب أكثر من 50 مللي ثانية). تفويت هذه الأهداف يُلاحظه المستخدمون. سقف 10 جيجابايت في HexaTransfer قابل للتحقيق داخل المتصفح لأن Web Crypto مع البث المجزّأ يُبقي المسار بأكمله كفوءاً.
جرّبها على hexatransfer.com — مجاني، بدون حساب، حتى 10 جيجابايت.
أرسل ملفات كبيرة بأمان مع تشفير من طرف إلى طرف
انقل ملفات حتى 10 جيجابايت مجاناً مع تشفير من طرف إلى طرف. لا حاجة لحساب. يتم تشفير ملفاتك في متصفحك قبل الرفع — لا أحد آخر يستطيع قراءتها.
إرسال ملف