Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Optimize Figma REST API performance with caching, partial fetches, and connection reuse. Use when experiencing slow API responses, reducing bandwidth for large files, or optimizing request throughput for Figma integrations. Trigger with phrases like "figma performance", "figma slow", "figma caching", "figma optimize", "figma large file".
.claude/skills/jeremylongshore-figma-performance-tuning/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 12% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 42% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 18% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 83% | 0% |
Optimize Figma REST API performance. Large Figma files can return multi-megabyte JSON responses. Key strategies: fetch only what you need, cache aggressively, and batch requests.
typescript// BAD: fetches the entire file tree (can be 10+ MB for large files) const file = await fetch(`https://api.figma.com/v1/files/${fileKey}`, { headers: { 'X-Figma-Token': token }, }).then(r => r.json()); // GOOD: use depth parameter to limit tree depth // depth=1 returns only pages (CANVAS nodes), not their children const fileMeta = await fetch( `https://api.figma.com/v1/files/${fileKey}?depth=1`, { headers: { 'X-Figma-Token': token } } ).then(r => r.json()); // GOOD: fetch only specific nodes you need const nodes = await fetch( `https://api.figma.com/v1/files/${fileKey}/nodes?ids=${nodeIds.join(',')}`, { headers: { 'X-Figma-Token': token } } ).then(r => r.json()); // GOOD: use plugin_data or branch_data params only when needed // By default, plugin data and branch data are NOT returned
typescriptimport { LRUCache } from 'lru-cache'; // File metadata changes rarely -- cache for 5 minutes const fileCache = new LRUCache<string, any>({ max: 100, ttl: 5 * 60 * 1000, // 5 minutes }); async function getCachedFile(fileKey: string, token: string) { const cached = fileCache.get(fileKey); if (cached) return cached; const file = await fetch( `https://api.figma.com/v1/files/${fileKey}?depth=1`, { headers: { 'X-Figma-Token': token } } ).then(r => r.json()); fileCache.set(fileKey, file); return file; } // Image URLs expire after 30 days -- cache them but with a shorter TTL const imageUrlCache = new LRUCache<string, string>({ max: 1000, ttl: 24 * 60 * 60 * 1000, // 1 day (well within 30-day expiry) }); async function getCachedImageUrl( fileKey: string, nodeId: string, format: string, token: string ): Promise<string | null> { const cacheKey = `${fileKey}:${nodeId}:${format}`; const cached = imageUrlCache.get(cacheKey); if (cached) return cached; const data = await fetch( `https://api.figma.com/v1/images/${fileKey}?ids=${nodeId}&format=${format}`, { headers: { 'X-Figma-Token': token } } ).then(r => r.json()); const url = data.images[nodeId]; if (url) imageUrlCache.set(cacheKey, url); return url; }
typescript// Instead of polling, use webhooks to know when to re-fetch // See figma-webhooks-events for full webhook setup async function handleFileUpdate(fileKey: string) { // Invalidate cached data for this file fileCache.delete(fileKey); // Proactively re-fetch commonly accessed data const token = process.env.FIGMA_PAT!; await getCachedFile(fileKey, token); console.log(`Cache invalidated and refreshed for ${fileKey}`); }
typescript// The /nodes endpoint accepts multiple IDs -- batch them // Max practical batch size: ~50-100 IDs per request async function batchFetchNodes( fileKey: string, nodeIds: string[], token: string, batchSize = 50 ): Promise<Map<string, any>> { const results = new Map<string, any>(); for (let i = 0; i < nodeIds.length; i += batchSize) { const batch = nodeIds.slice(i, i + batchSize); const ids = encodeURIComponent(batch.join(',')); const data = await fetch( `https://api.figma.com/v1/files/${fileKey}/nodes?ids=${ids}`, { headers: { 'X-Figma-Token': token } } ).then(r => r.json()); for (const [id, node] of Object.entries(data.nodes)) { results.set(id, node); } } return results; }
typescriptimport { Agent } from 'undici'; // Reuse HTTP connections to api.figma.com const figmaAgent = new Agent({ keepAliveTimeout: 30_000, keepAliveMaxTimeout: 60_000, connections: 5, }); // Use with Node.js 18+ built-in fetch async function optimizedFetch(path: string, token: string) { return fetch(`https://api.figma.com${path}`, { headers: { 'X-Figma-Token': token }, // @ts-ignore -- dispatcher is a Node.js fetch option dispatcher: figmaAgent, }); }
depth and nodes endpoints| Issue | Cause | Solution | |-------|-------|----------| | Stale cache | No invalidation | Use webhooks to invalidate on changes | | Out of memory | Caching full file JSON | Use depth=1 or nodes endpoint | | Slow image exports | Large batch, high scale | Reduce scale; batch in groups of 50 | | Expired image URLs | Cached URL older than 30 days | Set image cache TTL to <24h |
Measure the win from payload reduction (Step 1) on a real design-system file:
bashfor url in "files/${FIGMA_FILE_KEY}" "files/${FIGMA_FILE_KEY}?depth=1"; do curl -s -o /dev/null -w "%{size_download}B %{time_total}s ${url}\n" \ -H "X-Figma-Token: ${FIGMA_PAT}" "https://api.figma.com/v1/${url}" done
text41520883B 6.180s files/AbC123 ← full tree 38412B 0.310s files/AbC123?depth=1 ← 1000x smaller, 20x faster
Confirm the version-keyed cache (Step 2) short-circuits repeat fetches:
textGET file AbC123 cache MISS (version 1234567890) → fetched, cached GET file AbC123 cache HIT (version unchanged) → 0 API calls webhook FILE_UPDATE AbC123 → invalidated → next GET refetches
Batching and connection reuse details: references/batch-node-fetches.md, references/connection-reuse.md.
For cost optimization, see figma-cost-tuning.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 31,645 | 27,884 | -12% | 1 | 1 | 0% | 6,473 | 7,269 | +12% | 0 | 0 | — |
case-02 | fail→pass | 26,465 | 27,369 | +3% | 1 | 1 | 0% | 4,457 | 6,342 | +42% | 0 | 0 | — |
case-03 | fail→pass | 34,127 | 36,844 | +8% | 1 | 1 | 0% | 6,753 | 7,994 | +18% | 0 | 0 | — |
case-04 | pass→pass | 9,534 | 6,806 | -29% | 1 | 1 | 0% | 1,819 | 2,958 | +63% | 0 | 0 | — |
case-05 | pass→pass | 6,690 | 4,972 | -26% | 1 | 1 | 0% | 1,025 | 2,933 | +186% | 0 | 0 | — |
case-06 | fail→pass | 14,414 | 9,696 | -33% | 1 | 1 | 0% | 2,025 | 3,403 | +68% | 0 | 0 | — |
case-07 | fail→pass | 12,176 | 11,283 | -7% | 1 | 1 | 0% | 1,953 | 3,583 | +83% | 0 | 0 | — |
case-08 | pass→pass | 11,284 | 10,002 | -11% | 1 | 1 | 0% | 1,905 | 3,464 | +82% | 0 | 0 | — |
case-09 | pass→pass | 10,181 | 3,262 | -68% | 1 | 1 | 0% | 1,814 | 2,581 | +42% | 0 | 0 | — |
case-10 | fail→pass | 10,654 | 9,783 | -8% | 1 | 1 | 0% | 1,867 | 3,550 | +90% | 0 | 0 | — |
case-11 | pass→pass | 10,301 | 6,739 | -35% | 1 | 1 | 0% | 1,672 | 2,972 | +78% | 0 | 0 | — |
case-12 | pass→pass | 14,689 | 12,336 | -16% | 1 | 1 | 0% | 2,759 | 4,383 | +59% | 0 | 0 | — |
case-13 | fail→pass | 19,146 | 2,298 | -88% | 1 | 1 | 0% | 3,509 | 2,342 | -33% | 0 | 0 | — |
case-14 | pass→pass | 6,542 | 3,561 | -46% | 1 | 1 | 0% | 1,186 | 2,616 | +121% | 0 | 0 | — |
case-15 | pass→pass | 12,433 | 5,372 | -57% | 1 | 1 | 0% | 2,183 | 2,941 | +35% | 0 | 0 | — |
case-16 | pass→pass | 10,835 | 10,989 | +1% | 1 | 1 | 0% | 1,853 | 3,898 | +110% | 0 | 0 | — |
case-17 | pass→pass | 13,327 | 4,702 | -65% | 1 | 1 | 0% | 2,566 | 2,903 | +13% | 0 | 0 | — |
case-18 | fail→fail | 17,090 | 13,540 | -21% | 1 | 1 | 0% | 3,018 | 4,574 | +52% | 0 | 0 | — |
case-19 | fail→pass | 13,089 | 10,776 | -18% | 1 | 1 | 0% | 2,153 | 3,696 | +72% | 0 | 0 | — |
case-20 | pass→pass | 9,214 | 7,892 | -14% | 1 | 1 | 0% | 1,897 | 3,557 | +88% | 0 | 0 | — |
case-21 | fail→pass | 11,600 | 9,386 | -19% | 1 | 1 | 0% | 2,511 | 3,864 | +54% | 0 | 0 | — |
case-22 | pass→pass | 7,048 | 8,428 | +20% | 1 | 1 | 0% | 1,377 | 3,483 | +153% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.