Agent Skills: Canva Performance Tuning

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/canva-performance-tuning

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/canva-pack/skills/canva-performance-tuning

Skill Files

Browse the full folder contents for canva-performance-tuning.

Download Skill

Loading file tree…

plugins/saas-packs/canva-pack/skills/canva-performance-tuning/SKILL.md

Skill Metadata

Name
canva-performance-tuning
Description
|

Canva Performance Tuning

Overview

Optimize Canva Connect API performance. The REST API at api.canva.com/rest/v1/* has per-user rate limits and async operations (exports, uploads, autofills) that require polling.

Caching Strategy

Design Metadata Cache

import { LRUCache } from 'lru-cache';

// Design metadata changes infrequently — cache aggressively
const designCache = new LRUCache<string, any>({
  max: 500,
  ttl: 5 * 60 * 1000,  // 5 minutes
});

async function getDesignCached(designId: string, token: string) {
  const cached = designCache.get(designId);
  if (cached) return cached;

  const data = await canvaAPI(`/designs/${designId}`, token);
  designCache.set(designId, data);
  return data;
}

// IMPORTANT: Do NOT cache these — they expire quickly:
// - Thumbnail URLs: expire in 15 minutes
// - Edit/view URLs: expire in 30 days
// - Export download URLs: expire in 24 hours

Redis Cache for Distributed Systems

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

async function cachedCanvaCall<T>(
  key: string,
  fetcher: () => Promise<T>,
  ttlSeconds = 300
): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const result = await fetcher();
  await redis.setex(key, ttlSeconds, JSON.stringify(result));
  return result;
}

// Cache brand template list — rarely changes
const templates = await cachedCanvaCall(
  'canva:brand-templates:list',
  () => canvaAPI('/brand-templates', token),
  3600 // 1 hour
);

Pagination Optimization

// Canva uses continuation-based pagination
async function* paginateDesigns(
  token: string,
  opts: { ownership?: string; limit?: number } = {}
): AsyncGenerator<any> {
  let continuation: string | undefined;

  do {
    const params = new URLSearchParams({
      limit: String(opts.limit || 100),  // Max 100 per page
      ...(opts.ownership && { ownership: opts.ownership }),
      ...(continuation && { continuation }),
    });

    const data = await canvaAPI(`/designs?${params}`, token);

    for (const design of data.items) {
      yield design;
    }

    continuation = data.continuation; // undefined = last page
  } while (continuation);
}

// Usage — processes designs as they arrive
for await (const design of paginateDesigns(token, { ownership: 'owned' })) {
  console.log(`${design.title} (${design.id})`);
}

Export Polling Optimization

// Smart polling with progressive backoff
async function pollExport(exportId: string, token: string): Promise<string[]> {
  const delays = [500, 1000, 2000, 3000, 5000, 5000, 10000]; // Progressive backoff
  let attempt = 0;

  while (attempt < 20) { // Max ~60s total
    const { job } = await canvaAPI(`/exports/${exportId}`, token);

    if (job.status === 'success') return job.urls;
    if (job.status === 'failed') throw new Error(`Export failed: ${job.error?.message}`);

    const delay = delays[Math.min(attempt, delays.length - 1)];
    await new Promise(r => setTimeout(r, delay));
    attempt++;
  }

  throw new Error('Export polling timeout');
}

// Batch exports with concurrency control
import PQueue from 'p-queue';

const exportQueue = new PQueue({ concurrency: 3 });

async function batchExport(
  designIds: string[],
  format: object,
  token: string
): Promise<Map<string, string[]>> {
  const results = new Map<string, string[]>();

  await Promise.all(
    designIds.map(id =>
      exportQueue.add(async () => {
        const { job } = await canvaAPI('/exports', token, {
          method: 'POST',
          body: JSON.stringify({ design_id: id, format }),
        });
        const urls = await pollExport(job.id, token);
        results.set(id, urls);
      })
    )
  );

  return results;
}

Connection Optimization

import { Agent } from 'https';

// Keep-alive for connection reuse
const agent = new Agent({
  keepAlive: true,
  maxSockets: 10,
  maxFreeSockets: 5,
  timeout: 30000,
});

// Use with Node.js fetch or undici
const res = await fetch('https://api.canva.com/rest/v1/designs', {
  headers: { 'Authorization': `Bearer ${token}` },
  // @ts-expect-error — Node.js specific
  agent,
});

Performance Monitoring

async function measuredCanvaCall<T>(
  operation: string,
  fn: () => Promise<T>
): Promise<T> {
  const start = performance.now();
  try {
    const result = await fn();
    const ms = (performance.now() - start).toFixed(0);
    console.log(`[canva] ${operation}: ${ms}ms OK`);
    return result;
  } catch (error) {
    const ms = (performance.now() - start).toFixed(0);
    console.error(`[canva] ${operation}: ${ms}ms FAIL`, error);
    throw error;
  }
}

Performance Benchmarks

| Operation | Typical Latency | Rate Limit | |-----------|----------------|------------| | GET /users/me | 50-150ms | 10/min | | POST /designs | 200-500ms | 20/min | | GET /designs (list) | 100-300ms | 100/min | | POST /exports | 100-300ms (job start) | 75/5min | | Export completion | 2-15s (depending on size) | N/A | | POST /asset-uploads | 300-2000ms | 30/min | | POST /autofills | 500-3000ms (job start) | 60/min |

Error Handling

| Issue | Cause | Solution | |-------|-------|----------| | Stale cache | Long TTL | Reduce TTL or invalidate on write | | Export timeout | Large/complex design | Increase poll timeout | | Memory pressure | Cache too large | Set LRU max entries | | Connection refused | Pool exhausted | Increase maxSockets |

Resources

Next Steps

For cost optimization, see canva-cost-tuning.