Embracing the Combined Power of Worker Threads and Server Actions in Next.js

Embracing the Combined Power of Worker Threads and Server Actions in Next.js

Worker Threads in Node.js

A worker thread is a separate thread of execution that runs concurrently with the main thread in a program. It operates independently and can perform tasks in parallel, allowing the application to handle multiple operations simultaneously. The primary purpose of worker threads is to offload computationally intensive or time-consuming tasks from the main thread, preventing it from being blocked and ensuring that the application remains responsive.

In Node.js, worker threads are a feature introduced to enable parallelism in JavaScript applications. They provide a mechanism for running JavaScript code in separate threads, distinct from the main event loop. This is particularly useful in scenarios where certain operations, such as heavy computations, might otherwise cause the main thread to become unresponsive.

Worker threads operate in their isolated context, meaning they have their own set of variables and do not share the same global scope as the main thread. This isolation helps avoid conflicts and issues related to shared state between threads.

Typically, worker threads are employed when an application needs to perform CPU-bound tasks, such as data processing, image manipulation, or complex algorithmic computations. By distributing these tasks across multiple threads, the overall performance and efficiency of the application can be significantly improved, especially on multi-core systems where each thread can run on a separate core.

Benefits of a Worker Thread

Using worker threads in Node.js can bring several advantages to your application, particularly in scenarios where parallel processing or offloading heavy computations is beneficial. Here's a summary of why you should consider using worker threads in Node.js:

  • Concurrency and Parallelism: Worker threads enable concurrent execution of tasks, allowing your Node.js application to handle multiple operations simultaneously. Parallelism is achieved by leveraging the multi-core architecture of modern CPUs, improving overall performance.
  • Improved Performance: Offloading CPU-intensive tasks to worker threads prevents blocking the event loop in the main thread, ensuring responsiveness for other tasks. Enhanced performance is especially noticeable in applications that involve heavy computational work, such as image processing or complex data transformations.
  • Scalability: Worker threads contribute to better scalability by efficiently utilizing resources on multi-core systems. As your application grows, the ability to scale across multiple threads can help handle increased workloads more effectively.
  • Enhanced Responsiveness: By delegating resource-intensive tasks to worker threads, the main thread remains free to handle I/O operations, ensuring responsiveness and a smoother user experience.
  • Isolation and Safety: Worker threads operate in a separate context from the main thread, providing a level of isolation. This helps prevent global state conflicts and reduces the risk of errors caused by shared variables.
  • Optimized Resource Utilization: Efficiently utilizing available CPU resources through worker threads can lead to optimized resource consumption, making your application more efficient and cost-effective.
  • Support for CPU-bound Tasks: Worker threads are precious for CPU-bound tasks, such as cryptographic operations, image or video processing, and complex algorithmic computations.
  • Ease of Use: Node.js provides a simple API for creating and managing worker threads, making it relatively straightforward to implement parallelism in your application without complex setups.

What is Server Action?

From the official documentation of Next.js, we can say that Server Actions are asynchronous functions that are executed on the server. They can be used in Server and Client Components to handle form submissions and data mutations in Next.js applications. Server actions are super handy and easy to use.

Here are some reasons why you'll love Server Actions:

  • Effortless Data: Fetch data from APIs or databases with ease, directly from your components. No more juggling API routes and client-side fetches.
  • Secure Zone: Keep sensitive data safe by processing it on the server. Client-side code only receives the results, not the raw data.
  • Performance Boost: Pre-render pages with the data they need, leading to faster loading times and happier users.
  • Code Management: Organize your logic better by placing data fetching and mutations closer to the components that use them.

Let’s stop theories and dive into implementation

Let's initiate a Next.js project with

npx create-next-app@latest        

Now, I have cleared the main page like this,

// app/page.tsx

import Buttons from "./buttons";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <Buttons />
    </main>
  );
}        
// app/buttons.tsx

"use client";

import {
  myHeavyTaskInWorkerThread,
  myHeavyTaskInMainThread,
  myLightTaskInMainThread,
} from "./action";

const Buttons = () => (
  <div className="flex gap-3">
    <button
      className="bg-gray-700 p-3 rounded-lg hover:bg-black hover:border-white border"
      onClick={() => myHeavyTaskInWorkerThread()}
    >
      Call Heavy Task In Worker Thread
    </button>

    <button
      className="bg-gray-700 p-3 rounded-lg hover:bg-black hover:border-white border"
      onClick={() => myHeavyTaskInMainThread()}
    >
      Call Heavy Task In Main Thread
    </button>

    <button
      className="bg-gray-700 p-3 rounded-lg hover:bg-black hover:border-white border"
      onClick={() => myLightTaskInMainThread()}
    >
      Call Light Task In Main Thread
    </button>
  </div>
);

export default Buttons;        

Now my UI looks like this,

Now Let's explain the code a bit,

I have made a button component where I have three buttons. Each one of them will call a server action.

  1. myHeavyTaskInMainThread

export async function myHeavyTaskInMainThread() {
  console.log("Called myHeavyTaskInMainThread");
  let sum = 0;
  for (let i = 1; i <= 10000000000; i++) {
    sum += i;
  }
  console.log("Result from myHeavyTaskInMainThread:", sum, "\n");
  return sum;
}        

This is a simple task that will take some time to execute and will eventually block the main thread. We will see it in action soon.

2. myLightTaskInMainThread

export async function myLightTaskInMainThread() {
  console.log("Called myLightTaskInMainThread");
  let sum = 0;
  for (let i = 1; i <= 10; i++) {
    sum += i;
  }
  console.info("Result from myLightTaskInMainThread:", sum, "\n");
  return sum;
}        

This is a similar task that won't take much time to execute and will not block the main thread.

3. myHeavyTaskInWorkerThread

export async function myHeavyTaskInWorkerThread() {
  console.log("Called myHeavyTaskInWorkerThread");
  if (isMainThread) {
    const worker = new Worker("./app/worker.js", {
      workerData: {
        task: "performSum",
        targetValue: 10000000000,
      },
    });

    worker.on("message", (result) => {
      console.log("Result from myHeavyTaskInWorkerThread:", result, "\n");
    });
  }
}        

This is also a server action but unlike others, it will create a worker and execute a script that is equivalent to the heavy task on the main thread function.

// app/worker.js

const { parentPort } = require("worker_threads");

function aHeavyTask() {
  let sum = 0;
  for (let i = 1; i <= 10000000000; i++) {
    sum += i;
  }
  return sum;
}

const result = aHeavyTask();
parentPort.postMessage(result);        

Now, When I click on the button with the title "Call Heavy Task In Main Thread", it blocks the main thread for some time. During that time no matter how many times I click on the button with the title "Call light Task In Main Thread", it won't do anything which means it is now blocked and queued. As soon as the task is finished it will execute the tasks from the queue. but the same task if I call through the button with the title "Call Heavy Task In Worker Thread", it will not block the main thread and will keep executing the next commands. let's watch it in action.

Demo Video (Gif)

So in the video, you can see when I clicked on the heavy task on the main thread it blocked the other code executions and later once the heavy task was finished it executed the other task. It can be a huge issue if you have any heavy CPU-intensive task and it blocks other user's requests. But when we used the worker thread it kept working on the task but also parallelly continued executing other tasks.

Conclusion

In conclusion, harnessing Worker Threads and Server Actions empowers developers to craft more responsive, efficient, and scalable Next.js applications.

Key takeaways:

  • Worker Threads unshackle CPU-intensive tasks from the main thread, unlocking a new dimension of parallelism and responsiveness.
  • Server Actions streamline data fetching, mutations, and server-side logic within components, simplifying development and enhancing security.
  • Strategically combining these features fosters applications that deliver exceptional performance, user experience, and maintainability.

Ready to embrace the future of Next.js development?

  • Explore Worker Threads to optimize CPU-bound operations and safeguard responsiveness.
  • Adopt Server Actions to simplify data management, boost security, and streamline code structure.
  • Embrace these advancements to create a new generation of high-performance, user-centric web applications.

Unleash the full potential of Next.js and create web experiences that surpass expectations!

Zul Ikram Musaddik Rayat

Software Developer | Robotics & ML Enthusiast | Engineering Student

11 个月

This doesn't seem to work. The worker file is not separated during the build and gets compiled in the same file. The worker cannot find the file.

回复

要查看或添加评论,请登录

Md Sakibul Alam的更多文章

社区洞察

其他会员也浏览了