6 Ways To Optimize React App Performance Served by a Go Backend

Nik L. - Jan 10 - - Dev Community

Check out more articles:

  1. Building a Scalable Notification System with gRPC and Microservices
  2. React App is Slow? Here's What You Can Do.
  3. A Complete Guide on Notification Infrastructure for Modern Applications in 2023

Identifying Bottlenecks:

Before diving into optimizations, it's essential to identify the bottlenecks affecting the app's initial load time. Consider metrics like Time to First Byte (TTFB), Largest Contentful Paint (LCP), and server-side response times. With those data in hand, let's test out some strategies.

Caching Strategy:

Cache Control in Go:

To optimize the caching of React and third-party library bundles, implement cache control in the Go HTTP headers. The goal here is to cache React libraries and third-party libraries separately, if they form the majority of the frontend code. Conditionally set cache control based on the type of bundle requested.

package main

import (
    "net/http"
    "strings"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // Handle other routes...

        if strings.Contains(r.URL.Path, "/react-bundle/") {
            // Set cache control for React bundles
            w.Header().Set("Cache-Control", "public, max-age=604800") // 1 week
        } else if strings.Contains(r.URL.Path, "/third-party-bundle/") {
            // Set cache control for third-party bundles
            w.Header().Set("Cache-Control", "public, max-age=2592000") // 1 month
        }

        // Serve the requested file...
    })

    http.ListenAndServe(":8080", nil)
}
Enter fullscreen mode Exit fullscreen mode

This code snippet demonstrates how to conditionally set cache control based on the requested bundle type.

Gzip Compression:

Pre-compression with Webpack:

Explore pre-compressing JS files with a Webpack plugin to enhance gzip compression. This example integrates the CompressionPlugin to pre-compress JS and CSS files. Though it's impact may be marginal.

// webpack.config.js
const CompressionPlugin = require("compression-webpack-plugin");

module.exports = {
  // Other configurations...
  plugins: [
    new CompressionPlugin({
      filename: "[path][base].gz",
      algorithm: "gzip",
      test: /\.js$|\.css$/,
      threshold: 10240,
      minRatio: 0.8,
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

This webpack.config.js snippet shows how to integrate the CompressionPlugin to pre-compress JS and CSS files.

Image Optimization:

Optimizing image sizes is crucial for improving the Largest Contentful Paint (LCP) metric. Large images contribute significantly to slow load times. Tools like Squoosh provide a user-friendly interface for compressing and resizing images effectively.

Implementation:

  1. Squoosh Integration:

    • Visit Squoosh and upload images.
    • Experiment with compression settings to find the right balance between image quality and file size.
    • Download the optimized images and replace the existing ones in your React app.
  2. Webpack Image Compression:

    • Integrate image compression directly into your Webpack configuration.
   // webpack.config.js
   module.exports = {
     // Other configurations...
     module: {
       rules: [
         {
           test: /\.(png|jpe?g|gif)$/i,
           use: [
             {
               loader: 'image-webpack-loader',
               options: {
                 mozjpeg: {
                   progressive: true,
                   quality: 65,
                 },
                 optipng: {
                   enabled: false,
                 },
                 pngquant: {
                   quality: [0.65, 0.90],
                   speed: 4,
                 },
                 gifsicle: {
                   interlaced: false,
                 },
               },
             },
           ],
         },
       ],
     },
   };
Enter fullscreen mode Exit fullscreen mode

This example demonstrates how to integrate image compression into your Webpack build process.

Profiling and Benchmarking:

Profiling and benchmarking are essential for identifying performance bottlenecks and ensuring ongoing improvements. Continuous integration (CI) integration helps monitor performance over time.

Implementation:

  1. Go Benchmarking:
    • Create benchmark tests for critical functions in your Go backend.
    • Integrate benchmarks into your CI/CD pipeline.
    • Example benchmark:
   package main

   import (
     "testing"
   )

   func BenchmarkExampleFunction(b *testing.B) {
     for i := 0; i < b.N; i++ {
       // Call the function to be benchmarked
       ExampleFunction()
     }
   }
Enter fullscreen mode Exit fullscreen mode

Run benchmarks using go test -bench=..

  1. Frontend Benchmarking:
    • Utilize tools like Lighthouse CI for benchmarking frontend performance.
    • Integrate Lighthouse CI scripts into your CI/CD pipeline.
    • Example Lighthouse CI script:
   # .lighthouseci.yml
   ci:
     collect:
       url: "http://your-app-url"
       settings:
         emulatedFormFactor: "desktop"
     assert:
       assertions:
         - ["performance-budget", "speed-index", ">= 0"]
Enter fullscreen mode Exit fullscreen mode

Customize the script based on your performance goals.

NGINX Frontend:

NGINX can serve as a frontend proxy for the Go backend, handling static content, cache control, and pre-compressed files.

Implementation:

  1. NGINX Configuration:
    • Install NGINX and configure it to proxy requests to your Go backend.
    • Set up static file serving and implement cache control.
    • Example NGINX configuration:
   server {
     listen 80;
     server_name your-domain.com;

     location / {
       proxy_pass http://localhost:8080; # Your Go backend address
       # Additional proxy settings...
     }

     location /static/ {
       alias /path/to/your/static/files;
       # Cache control settings...
     }
   }
Enter fullscreen mode Exit fullscreen mode

Customize the configuration based on your specific setup.

Client-Side A/B Testing:

Conducting A/B testing on the client side involves temporarily disabling scripts or third-party integrations to identify their impact on performance.

Implementation:

  1. A/B Testing Framework:
    • Use an A/B testing framework like Optimizely or Google Optimize.
    • Create experiments that involve temporarily disabling specific scripts or integrations.
    • Analyze performance metrics for each variant to identify bottlenecks.

Monitor Performance:

Monitoring performance using tools like Chrome's Lighthouse and Google PageSpeed Insights is crucial. Focusing on the p99 (99th percentile) helps identify the slowest requests in the system.

Implementation:

  1. Lighthouse and PageSpeed Insights:
    • Regularly run Lighthouse and PageSpeed Insights tests on your React app.
    • Analyze the generated reports for suggestions and areas of improvement.
    • Focus on the p99 metric to address the slowest requests and optimize accordingly.

Regularly revisit and adapt these optimizations as your application evolves.


Similar to this, I personally run a developer-led community on Slack. Where we discuss these kinds of implementations, integrations, some truth bombs, weird chats, virtual meets, and everything that will help a developer remain sane ;) Afterall, too much knowledge can be dangerous too.

I'm inviting you to join our free community, take part in discussions, and share your freaking experience & expertise. You can fill out this form, and a Slack invite will ring your email in a few days. We have amazing folks from some of the great companies (Atlassian, Scaler, Cisco, IBM and more), and you won't wanna miss interacting with them. Invite Form


You may want to check out a seamless way of integrating your notification infrastructure into your applications without needing code & maintenance. Available in all popular languages (Go, Node, Java, Python) with frontend support (React, Angular, Flutter).

GitHub logo suprsend / suprsend-go

SuprSend SDK for go

suprsend-go

SuprSend Go SDK

Installation

go get github.com/suprsend/suprsend-go
Enter fullscreen mode Exit fullscreen mode

Usage

Initialize the SuprSend SDK

import (
    "log"

    suprsend "github.com/suprsend/suprsend-go"
)

func main() {
    opts := []suprsend.ClientOption{
        // suprsend.WithDebug(true),
    }
    suprClient, err := suprsend.NewClient("__api_key__", "__api_secret__", opts...)
    if err != nil {
        log.Println(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Trigger Workflow

package main
import (
    "log"

    suprsend "github.com/suprsend/suprsend-go"
)

func main() {
    // Instantiate Client
    suprClient, err := suprsend.NewClient("__api_key__", "__api_secret__")
    if err != nil {
        log.Println(err)
        return
    }
    // Create WorkflowTriggerRequest body
    wfReqBody := map[string]interface{}{
        "workflow": "workflow-slug",
        "recipients": []map[string]interface{}{
            {
                "distinct_id": "0f988f74-6982-41c5-8752-facb6911fb08",
                // if $channels is present, communication will be tried on mentioned channels only (for this request).
                // "$channels": []string{"email"},
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .