تصور کنید یک اپلیکیشن چت در یک بعدازظهر نزدیک به ۶ میلیون توکن مصرف کند، بدون اینکه حتی یک پاسخ جدید تولید کرده باشد. این اتلاف عظیم زمانی رخ میدهد که تسترها و کاربران به طور مکرر فرمها را رفرش میکنند یا روی پرامپتهای قبلی کلیک میکنند و هر بار، یک درخواست استریم (Streaming) کامل و یکسان به سرور ارسال میشود.
به نقل از گزارشی که در ۱۸ آگوست ۲۰۲۶ در dev.to منتشر شد، سرور در هر بار درخواست وضعیت ۲۰۰ (OK) را برمیگرداند و این ناکارآمدی تا زمانی که نمودارهای سهمیه توکنها جهش شدیدی نکردند، پنهان ماند. این مشکل دقیقاً همانجایی است که معماریهای فرانتاند با یک چالش جدی روبرو میشوند: اکثر آنها هر پاسخ ۲۰۰ را به عنوان دادهای تازه میبینند و هیچ تفاوتی بین یک درخواست تکراری و یک درخواست واقعاً جدید قائل نمیشوند. این نوع اتلاف منابع در مقیاس بزرگتر، یادآور تحلیل ما درباره تلههای سیستمهای خودکار است که در آن یک نشست تنها در یک شب ۱۳۶ میلیون توکن را میسوزاند.
وقتی کاربر یک پرامپت یکسان را دوباره میفرستد، در تب Network مرورگر درخواستهای POST مکرر به نقطه اتصال /api/chat دیده میشود. هر درخواست، بدنه JSON و هدرهای یکسانی دارد و مدل را مجبور میکند توالی توکنها را از ابتدا تولید کند. هر پاسخ، اتصال را برای مدت زمانی تقریباً یکسان باز نگه میدارد. در مدلهای رایگان با سهمیه محدود (Finite Token Pool)، این رفتار به شدت هزینهبر و در برخی موارد غیرقابل تحمل است.
همانطور که در تحلیلهای قبلی ما دربارهی بهینهسازی هزینههای استنتاج اشاره کردیم، انتقال بار پردازشی از سرور به کلاینت کلید کاهش هزینههاست. برای حل این مشکل، توسعهدهندگان میتوانند از یک سرویسورکر (Service Worker) — شبیه به یک نگهبان در ورودی مرورگر که درخواستها را قبل از خروج بررسی میکند — استفاده کنند تا به جای متن پرامپت، کل جریان پاسخ (Completed Stream) را کش (Cache) کند. سرویسورکر به عنوان یک پروکسی عمل میکند که درخواستها را پیش از خروج از مرورگر رهگیری میکند. اگر یک تکرار دقیق شناسایی شود، ورکر بلافاصله پاسخ ذخیرهشده را برمیگرداند. این سازوکار فراخوانی مدل را برای پرامپتهای جدید تغییر نمیدهد، بلکه فقط مسیر درخواستهای تکراری را کوتاه (Short-circuit) میکند. این رویکرد مشابه استفاده از Sidecar در پایپلاینهای CI است که با کشینگ پاسخها از سوختن سهمیههای مدلهای زبانی جلوگیری میکند.
طبق مستندات فنی این پیادهسازی، سازوکار دقیق آن بر چهار محور استوار است تا دقت و عملکرد سیستم تضمین شود:
- اثر انگشت بدنه (Body Fingerprinting): سیستم با استفاده از
crypto.subtle.digest(SHA-256)، ترکیبی از URL کامل درخواست و بدنه JSON را هش میکند. این کار برای جلوگیری از تداخل (Collision) در مواردی است که پرامپتهای مختلف از یک URL مشترک استفاده میکنند اما محتوای بدنه آنها متفاوت است. در کد پیادهسازی شده، از فرمولrequest.url + '|' + bodyبرای ایجاد این اثر انگشت استفاده شده است. - فیلتر کردن هدرها: برای جلوگیری از آلودگی کش (Cache Pollution)، فقط هدرهایی که بر محتوا اثر میگذارند، مانند
acceptوcontent-typeدر نظر گرفته میشوند. هدرهای متغیر احراز هویت (Authentication Headers) که در هر درخواست تغییر میکنند، از کلید کش حذف شدهاند تا باعث ایجاد ورودیهای تکراری و بیمورد نشوند. - دوگانه کردن استریم (Stream Teeing): چون
response.bodyیکReadableStreamاست، سرویسورکر از متدcacheable.clone()استفاده میکند. این کار باعث «دوگانه شدن» استریم میشود؛ به این معنا که مرورگر میتواند تکههای داده را در لحظه برای کلاینت ارسال کند و همزمان، شاخه دیگر استریم را در حافظه Cache Storage تخلیه و ذخیره کند. در نتیجه، کاربر همچنان دادهها را به صورت زنده دریافت میکند و کش در پسزمینه پر میشود. - مدیریت حافظه: برای جلوگیری از اتمام فضای دیسک کاربر، تابعی به نام
trimCacheپیادهسازی شده است. این تابع از یک حد مجاز به نامMAX_CACHE_ENTRIESبا مقدار ۴۰ ورودی استفاده میکند و با استفاده ازkeys.shift()، قدیمیترین رکوردها را ابتدا حذف میکند تا حجم ذخیرهسازی در محدوده مجاز مرورگر باقی بماند.
در لایه اجرایی، منطق سرویسورکر در فایل sw.js با نام کش chat-cache-v1 و ریشه ذخیرهسازی /__chat_cache/ تعریف شده است. این کد به طور خاص برای رویداد fetch گوش میدهد و درخواستهای POST را که به /chat ختم میشوند هدف قرار میدهد.
وقتی یک درخواست رهگیری میشود، تابع handleChatRequest بررسی میکند که آیا تطابقی در کش وجود دارد یا خیر. اگر تطابقی پیدا شود، هدر X-Chat-Cache: HIT به پاسخ اضافه میشود. در غیر این صورت، پاسخ از شبکه دریافت شده، با هدر X-Chat-Cache: MISS علامتگذاری میشود و با استفاده از event.waitUntil به صورت غیرهمزمان در حافظه ذخیره میگردد؛ البته این ذخیرهسازی تنها در صورتی انجام میشود که وضعیت پاسخ OK باشد و از نوع 206 (Partial Content) نباشد.
اما کش کردن پاسخهای استریم یک چالش جدید در تجربه کاربری (UX) و دسترسیپذیری (Accessibility) ایجاد میکند: سرعت. پاسخهای کششده چنان سریع میرسند که صفحهخوانها (Screen Readers) ممکن است انتقال وضعیت از حالت «در حال تولید...» (Streaming) به «پاسخ نهایی» را تشخیص ندهند. اگر رابط کاربری ابتدا متن status.textContent = 'Streaming...' را نمایش دهد و بلافاصله آن را با پاسخ جایگزین کند، صفحهخوان ممکن است هرگز این تغییر وضعیت را برای کاربر نگوید.
برای حل این مشکل، توسعهدهنده از هدر سفارشی X-Chat-Cache استفاده کرد تا فرانتاند بتواند صراحتاً ناحیه وضعیت را بهروزرسانی کند:
const cacheState = response.headers.get('X-Chat-Cache');
const status = document.getElementById('stream-status');
status.textContent = cacheState === 'HIT' ? 'Cached response loaded' : 'Streaming new response';
status.setAttribute('role', 'status');
این بهروزرسانی کوچک، یک ترفند عملکردی نامرئی را به یک تغییر وضعیت تبدیل میکند که کاربران کیبورد و صفحهخوان میتوانند آن را بشنوند. بدون این تغییر، یک پاسخ کششده ممکن است به گونهای به نظر برسد که صفحه به طور ناگهانی پریده است، که این یکی از خطاهای رایج در ممیزیهای دسترسیپذیری (Accessibility Audits) است.
این الگو با استفاده از MonkeyCode تست شد که یک لایه سرور رایگان و سهمیه ۳۰ میلیون توکن ارائه میدهد. توسعهدهنده از این سرور برای میزبانی صفحه و سرویسورکر استفاده کرد و دمو را از طریق یک متغیر محیطی (AI_UPSTREAM_URL) به یک نقطه اتصال بالادستی متصل نمود. در حالی که این روش بر لایه کلاینت تمرکز دارد، ابزارهایی مانند Claude Code با پیادهسازی کشینگ در لایههای عمیقتر، پتانسیل کاهش هزینههای API تا ۸۴ درصد را دارند.
برنامه تست شامل مراحل زیر بود:
- اجرای دمو به صورت محلی با دستور
npx serve .و تایید ثبت سرویسورکر در Chrome DevTools در مسیر Application > Service Workers. - تایید دریافت
X-Chat-Cache: MISSدر اولین فراخوانی وX-Chat-Cache: HITدر فراخوانی دوم. - اطمینان از اینکه در فراخوانی دوم، هیچ درخواست جدیدی به ارائهدهنده مدل در لایه بالادستی نمیرسد.
- تایید وجود ورودی در مسیر Application > Cache Storage > chat-cache-v1.
- پاک کردن کش از طریق
caches.delete('chat-cache-v1')برای اطمینان از اینکه مدل دوباره فراخوانی میشود. - تست با VoiceOver (در macOS) و NVDA (در ویندوز) برای اطمینان از اینکه ناحیه وضعیت عبارت «Cached response loaded» را اعلام میکند.
با این حال، توسعهدهندگان باید مراقب باشند و از کش کردن پاسخهایی که حاوی دادههای شخصی، اطلاعات حساب کاربری یا هر چیزی است که توسط هدر Cache-Control: no-store کنترل میشود، خودداری کنند.
سایر ملاحظات فنی عبارتند از:
- تأخیر فراخوانی اول: استریمهای طولانی باعث افزایش تأخیر در اولین فراخوانی میشوند تا زمانی که ورودی کش در دسترس قرار گیرد. کلاینت استریم اصلی را میبیند، اما کش تا زمان تکمیل کامل پاسخ قابل استفاده نیست.
- بار پردازشی CPU: ایجاد هش اثر انگشت، هزینه پردازشی بسیار اندکی به هر درخواست اضافه میکند، هرچند این مقدار در مقایسه با هزینه یک فراخوانی مدل، ناچیز است.
- بودجه توکن: پرامپتهای اول-بار همچنان توکن مصرف میکنند. این سیستم تنها زمانی کمک میکند که کاربران درخواستهای دقیقاً یکسانی را تکرار کنند.
این رویکرد، بار تکرار را از API گرانقیمت مدل به حافظه ارزانقیمت مرورگر منتقل میکند. با کوتاه کردن مسیر درخواستهای تکراری، توسعهدهندگان میتوانند بدون تغییر در تجربه کاربر برای پرامپتهای جدید، از بودجه توکنهای خود محافظت کنند.
گام بعدی شما
- اگر از APIهای گرانقیمت استفاده میکنید، لایه سرویسورکر را برای شناسایی درخواستهای تکراری در فرانتاند پیاده کنید.
- برای بهبود دسترسیپذیری (Accessibility)، حتماً وضعیت بارگذاری از کش را برای صفحهخوانها اعلان کنید.
- سقف تعداد ورودیهای کش (
MAX_CACHE_ENTRIES) را بر اساس حجم دادههای اپلیکیشن خود تنظیم کنید.
اما داستان سختافزاری این تحول حتی شگفتانگیزتر است — به تحلیل ما دربارهی تراشههای Blackwell مراجعه کنید.




گفتگو