The Power of Generator Functions in TypeScript: Optimizing eCommerce Product Recommendations
When building an eCommerce platform, one of the biggest challenges is efficiently handling large datasets — whether it’s products, user data, or recommendations. Memory management and performance optimization become crucial, especially when dealing with high traffic volumes and large catalogs. One solution that can dramatically improve performance in these scenarios is the use of generator functions in TypeScript.
In this post, we’ll explore what generator functions are, why they’re useful, and how they can be leveraged to optimize a real-world eCommerce use case: handling personalized product recommendations.
What is a Generator Function?
A generator function in TypeScript (and JavaScript) is a special kind of function that pauses execution and allows you to yield results one at a time. Unlike regular functions, which execute and return all results at once, a generator function can be paused and resumed, producing multiple values over time, without needing to return them all at once. This makes them particularly useful when you want to process large datasets or streams of data incrementally.
Syntax of a Generator Function
A generator function is defined using the function*
syntax, and it uses the yield
keyword to return values one by one:
function* productGenerator() {
yield "Product 1";
yield "Product 2";
yield "Product 3";
}
generator functions can be consumed in two primary ways:
- Using the
next()
function: This is the manual way of stepping through the generator's values one by one. - Using a
for...of
loop: This is a more convenient way to automatically iterate through the values yielded by the generator until it's done.
1. Using the next()
Function
The next()
function is used to manually move through the values yielded by the generator. Each time you call next()
, it resumes the generator from where it last paused and returns the next value.
const gen = productGenerator();
console.log(gen.next()); // { value: 'Product 1', done: false }
console.log(gen.next()); // { value: 'Product 2', done: false }
console.log(gen.next()); // { value: 'Product 3', done: false }
console.log(gen.next()); // { value: undefined, done: true } // Generator is finished
2. Using a for...of
Loop
The for...of
loop provides a simpler, cleaner way to iterate through all values yielded by a generator. It automatically calls next()
behind the scenes, until the generator is done, so you don’t need to manually handle next()
calls or check the done
property.
for (let product of productGenerator()) {
console.log(product); // Automatically logs each yielded product
}
Unlike traditional functions that run from start to finish, each gen.next()
call moves the execution to the next yield
point and pauses. This allows you to control the flow of data and handle large datasets efficiently, processing them incrementally rather than all at once.
When you use a generator function and the yield
keyword, the previous values that have already been yielded are no longer kept in memory unless you explicitly store them. This is one of the main benefits of generator functions—they don’t accumulate all the yielded values in memory.
Why Use Generator Functions?
Generator functions are incredibly useful in scenarios where:
- Memory management is critical, such as when processing large datasets.
- You need lazy evaluation, where items are produced only when needed.
- Asynchronous tasks need to be managed in a controlled flow.
- You want to process a sequence of data incrementally, rather than loading it all into memory at once.
In the context of eCommerce, these features come in handy when you’re working with product listings, personalized recommendations, or inventory management — essentially any time you’re handling a large amount of data. By yielding data one item at a time, you keep your memory usage low and improve overall performance.
Use Case: Optimizing Product Recommendations in eCommerce
Imagine an eCommerce platform that offers personalized product recommendations to users. The platform pulls product data from multiple sources, such as a recommendation engine, cross-selling APIs, or related products databases. Each source returns products that need to be serialized, validated, and rendered for the frontend.
The challenge here is:
- High memory usage: If all product data from every source is loaded into memory before processing, you could easily run into performance bottlenecks.
- Multiple loops: You might end up iterating over the same data more than once, first to serialize and validate it, and then to push it into a component for rendering.
Instead of loading everything into memory at once, we can use generator functions to process the data incrementally. This reduces memory usage and improves performance.
Traditional (Inefficient) Approach
Let’s first look at a traditional approach where you would fetch and process all products from different sources at once:
public async getRecommendations(): Promise<ProductCell[]> {
const sourceAProducts = await this.fetchFromSourceA();
const sourceBProducts = await this.fetchFromSourceB();
const allProducts = [...sourceAProducts, ...sourceBProducts];
const productCells = [];
for (const product of allProducts) {
const productCell = this.validateProduct(product);
if (productCell) {
productCells.push(productCell.value);
}
}
return productCells;
}
Problems with this Approach:
- Memory overload: All the products from both sources are fetched and stored in memory at the same time.
- Multiple loops: After fetching all products, we loop over them again for serialization and validation, which is inefficient.
Optimized Solution: Using Generator Functions
By using generator functions, we can serialize, validate, and yield products one at a time, rather than loading everything into memory. Here’s how we can optimize this workflow:
public *recommendationProducts(
sources: (() => Promise<Sources[]>)[]
): Generator<ProductCell> {
// Iterate over each source of recommendations
for (const fetchSource of sources) {
const products = await fetchSource(); // Fetch products from the current source
for (let i = 0; i < products.length; i++) {
const productCell = this.validateProduct(
products[i],
);
// Yield only valid products
if (productCell.success) {
yield productCell.value;
}
}
}
}
How This Works:
- Lazy Loading: Instead of fetching all products upfront, we fetch, serialize, and validate each product one by one.
- Memory Efficiency: By yielding each product immediately after processing, we avoid keeping all products in memory at once.
- Improved Flow: This generator function allows us to efficiently combine product data from multiple sources without overloading memory.
Consuming the Generator Function
Now that we have our generator function, let’s see how we would consume it and push the validated products to the frontend:
public async getProductRecommendations(): Promise<ProductComponent[]> {
const sources = [
() => this.fetchFromSourceA(),
() => this.fetchFromSourceB(),
() => this.fetchFromSourceC(),
];
const result: ProductComponent[] = [];
// Use the generator function to process recommendations from all sources
for (const productCell of this.recommendationProducts(sources)) {
result.push({ productCell }); // Push each validated product into the result array
}
return result;
}
Benefits of Using a Generator Function in this Case:
- Memory Efficiency: Products are processed one at a time, which significantly reduces memory usage, especially for large datasets.
- Faster Rendering: By yielding each product as soon as it’s ready, we can render recommendations incrementally, improving perceived performance.
- No Double Iteration: The generator handles serialization, validation, and pushing to the result list in one pass, avoiding redundant loops.
Final Thoughts
Using generator functions in TypeScript is an incredibly powerful technique when you need to handle large datasets efficiently. In the context of eCommerce, where product listings and recommendations are critical to user experience, optimizing memory usage and performance is key.
By yielding products one by one, we avoid memory overload, eliminate redundant loops, and provide a smoother experience for end users. Whether you’re building personalized product recommendations, infinite scroll product listings, or complex inventory systems, generator functions offer an elegant solution to optimize your workflow.
Generators may seem like a small change, but in high-scale environments, they can make a huge difference in performance and scalability. Give them a try in your next project!