Skillshub canva-performance-tuning

install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/canva-performance-tuning" ~/.claude/skills/comeonoliver-skillshub-canva-performance-tuning && rm -rf "$T"
manifest: skills/jeremylongshore/claude-code-plugins-plus-skills/canva-performance-tuning/SKILL.md
source content

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

OperationTypical LatencyRate Limit
GET /users/me50-150ms10/min
POST /designs200-500ms20/min
GET /designs (list)100-300ms100/min
POST /exports100-300ms (job start)75/5min
Export completion2-15s (depending on size)N/A
POST /asset-uploads300-2000ms30/min
POST /autofills500-3000ms (job start)60/min

Error Handling

IssueCauseSolution
Stale cacheLong TTLReduce TTL or invalidate on write
Export timeoutLarge/complex designIncrease poll timeout
Memory pressureCache too largeSet LRU max entries
Connection refusedPool exhaustedIncrease maxSockets

Resources

Next Steps

For cost optimization, see

canva-cost-tuning
.