Learning Objectives

  • Understand sequential vs parallel execution
  • Learn when to use each approach
  • Master performance optimization techniques
  • Implement hybrid execution strategies

Sequential Execution

Operations run one after another, each waiting for the previous to complete.

Sequential (Slow):

async function sequential() {
  const user = await fetchUser(1);      // Wait 1s
  const posts = await fetchPosts(1);    // Wait 1s
  const comments = await fetchComments(1); // Wait 1s
  return { user, posts, comments };
}
// Total time: 3 seconds

Parallel Execution

Operations run simultaneously, all starting at once.

Parallel (Fast):

async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1)
  ]);
  return { user, posts, comments };
}
// Total time: 1 second (fastest operation)

Performance Comparison

// Measure sequential
console.time('Sequential');
await sequential();
console.timeEnd('Sequential'); // ~3000ms

// Measure parallel
console.time('Parallel');
await parallel();
console.timeEnd('Parallel'); // ~1000ms

// 3x faster! 🚀

When to Use Sequential

1. Dependencies Between Operations

async function processOrder(orderId) {
  // Must be sequential - each depends on previous
  const order = await fetchOrder(orderId);
  const validated = await validateOrder(order);
  const payment = await processPayment(validated);
  const confirmation = await sendConfirmation(payment);
  return confirmation;
}

2. Rate Limiting

async function processItemsSequentially(items) {
  const results = [];
  
  for (const item of items) {
    const result = await processItem(item);
    results.push(result);
    await delay(100); // Rate limit: 10 per second
  }
  
  return results;
}

3. Resource Constraints

async function processLargeFiles(files) {
  // Process one at a time to avoid memory issues
  for (const file of files) {
    await processFile(file);
    // File is processed and memory freed before next
  }
}

When to Use Parallel

1. Independent Operations

async function loadDashboard(userId) {
  // All independent - can run in parallel
  const [user, notifications, settings, activity] = await Promise.all([
    fetchUser(userId),
    fetchNotifications(userId),
    fetchSettings(userId),
    fetchActivity(userId)
  ]);
  
  return { user, notifications, settings, activity };
}

2. Batch Processing

async function fetchMultipleUsers(userIds) {
  return await Promise.all(
    userIds.map(id => fetchUser(id))
  );
}

3. Maximum Performance

async function preloadResources() {
  await Promise.all([
    loadImages(),
    loadFonts(),
    loadScripts(),
    fetchInitialData()
  ]);
}

Comparison Table

Aspect Sequential Parallel
Speed Slower (sum of all times) Faster (longest operation)
Resource Usage Lower (one at a time) Higher (all at once)
Dependencies Handles dependencies Operations must be independent
Error Handling Stops at first error All run, fails if any fails
Use Case Dependent operations Independent operations

Hybrid Approaches

1. Partial Parallelization

async function getUserProfile(userId) {
  // Step 1: Fetch user (required first)
  const user = await fetchUser(userId);
  
  // Step 2: Fetch related data in parallel
  const [posts, followers, following] = await Promise.all([
    fetchPosts(user.id),
    fetchFollowers(user.id),
    fetchFollowing(user.id)
  ]);
  
  return { user, posts, followers, following };
}

2. Batched Parallel Processing

async function processBatches(items, batchSize = 5) {
  const results = [];
  
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    
    // Process batch in parallel
    const batchResults = await Promise.all(
      batch.map(item => processItem(item))
    );
    
    results.push(...batchResults);
  }
  
  return results;
}

// Process 100 items in batches of 10
await processBatches(items, 10);

3. Race with Fallback

async function fetchWithFallback() {
  try {
    // Try primary and backup in parallel
    return await Promise.race([
      fetchFromPrimary(),
      delay(2000).then(() => fetchFromBackup())
    ]);
  } catch (error) {
    return getDefaultData();
  }
}

Advanced Patterns

Controlled Concurrency

async function mapWithConcurrency(items, fn, concurrency = 3) {
  const results = [];
  const executing = [];
  
  for (const item of items) {
    const promise = Promise.resolve().then(() => fn(item));
    results.push(promise);
    
    if (concurrency <= items.length) {
      const e = promise.then(() => {
        executing.splice(executing.indexOf(e), 1);
      });
      executing.push(e);
      
      if (executing.length >= concurrency) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// Process with max 3 concurrent operations
await mapWithConcurrency(userIds, fetchUser, 3);

Progressive Loading

async function loadProgressively(items, onProgress) {
  const results = [];
  let completed = 0;
  
  const promises = items.map(async (item, index) => {
    const result = await processItem(item);
    completed++;
    onProgress(completed, items.length);
    return result;
  });
  
  return await Promise.all(promises);
}

// Usage
await loadProgressively(items, (completed, total) => {
  console.log(`Progress: ${completed}/${total}`);
});

Real-World Example: E-commerce Checkout

async function processCheckout(cart, user) {
  // Step 1: Validate in parallel
  const [cartValid, userValid, inventoryValid] = await Promise.all([
    validateCart(cart),
    validateUser(user),
    checkInventory(cart.items)
  ]);
  
  if (!cartValid || !userValid || !inventoryValid) {
    throw new Error('Validation failed');
  }
  
  // Step 2: Sequential payment processing
  const paymentIntent = await createPaymentIntent(cart.total);
  const payment = await processPayment(paymentIntent);
  
  // Step 3: Parallel post-payment operations
  const [order, receipt, inventory] = await Promise.all([
    createOrder(cart, payment),
    generateReceipt(payment),
    updateInventory(cart.items)
  ]);
  
  // Step 4: Send notifications (don't wait)
  sendConfirmationEmail(user.email, order).catch(console.error);
  
  return { order, receipt };
}

Performance Tips

  1. Identify independent operations - Run them in parallel
  2. Measure actual performance - Don't assume
  3. Consider network/server limits - Don't overwhelm
  4. Use batching for large datasets - Control concurrency
  5. Balance speed vs resources - More parallel isn't always better

Common Mistakes

❌ Unnecessary Sequential Execution

// Bad: Sequential when parallel is possible
async function bad() {
  const a = await fetchA(); // 1s
  const b = await fetchB(); // 1s
  const c = await fetchC(); // 1s
  return [a, b, c]; // 3s total
}

// Good: Parallel execution
async function good() {
  return await Promise.all([
    fetchA(),
    fetchB(),
    fetchC()
  ]); // 1s total
}

❌ Too Much Parallelization

// Bad: Overwhelming the server
async function bad(userIds) {
  return await Promise.all(
    userIds.map(id => fetchUser(id)) // 1000 requests at once!
  );
}

// Good: Controlled batching
async function good(userIds) {
  return await processBatches(userIds, 10);
}

Key Takeaways

  • ✅ Use parallel for independent operations
  • ✅ Use sequential for dependent operations
  • ✅ Parallel execution is much faster
  • ✅ Consider resource constraints
  • ✅ Use batching for large datasets
  • ✅ Measure performance to verify improvements

Next Steps

Now that you've mastered async/await, let's explore advanced Promise patterns!