Skip to main content
Server-side rendering (SSR) is one of Jaspr’s core features, allowing you to render components on the server and send HTML to the client for better SEO and initial page load performance.

How SSR works in Jaspr

When a request comes to your Jaspr server:
1

Component rendering

Jaspr renders your components to HTML on the server
2

HTML generation

The rendered HTML is embedded in a complete HTML document
3

Client hydration

The browser loads the HTML and JavaScript to make components interactive
4

Interactivity

Client-side code takes over, handling events and dynamic updates

Creating an SSR project

Create a new Jaspr project with server-side rendering:
jaspr create my_ssr_app --mode server
This generates a project structure with:
  • lib/app.dart - Your main application component
  • web/main.dart - Client entry point for hydration
  • bin/server.dart - Server entry point

Project structure

my_ssr_app/
├── lib/
│   ├── app.dart           # Application component
│   └── components/        # Reusable components
├── web/
│   └── main.dart          # Client entry (hydration)
├── bin/
│   └── server.dart        # Server entry
├── jaspr_options.yaml     # Jaspr configuration
└── pubspec.yaml

Server entry point

The server entry point in bin/server.dart sets up your application:
import 'package:jaspr/server.dart';
import 'package:my_ssr_app/app.dart';

void main() {
  runApp(
    Document(
      title: 'My SSR App',
      head: [
        meta(charset: 'utf-8'),
        meta(name: 'viewport', content: 'width=device-width, initial-scale=1'),
      ],
      body: App(),
    ),
  );
}
The runApp() function on the server automatically:
  • Sets up an HTTP server on port 8080 (configurable)
  • Renders components to HTML for each request
  • Serves the client JavaScript bundle
  • Handles hot reload in development mode

Client entry point

The client entry point in web/main.dart hydrates the server-rendered HTML:
import 'package:jaspr/browser.dart';
import 'package:my_ssr_app/app.dart';

void main() {
  runApp(App());
}
The component tree in web/main.dart must match the server-rendered tree from bin/server.dart (excluding the Document wrapper).

Making components server-only

Some components should only run on the server. Use AsyncStatelessComponent for server-only logic:
import 'package:jaspr/jaspr.dart';

class UserProfile extends AsyncStatelessComponent {
  final String userId;
  
  const UserProfile({required this.userId});
  
  @override
  Future<Component> build(BuildContext context) async {
    // This code only runs on the server
    final user = await fetchUserFromDatabase(userId);
    
    return div([
      h2([text(user.name)]),
      p([text(user.email)]),
    ]);
  }
}
AsyncStatelessComponent is perfect for:
  • Database queries
  • API calls to internal services
  • File system operations
  • Any server-only dependencies

Client-side components

Mark components that should only run on the client with @client:
import 'package:jaspr/jaspr.dart';

@client
class InteractiveMap extends StatefulComponent {
  @override
  State<InteractiveMap> createState() => _InteractiveMapState();
}

class _InteractiveMapState extends State<InteractiveMap> {
  @override
  Component build(BuildContext context) {
    // This component is replaced with a placeholder during SSR
    // and fully rendered on the client
    return div([text('Map will load here')]);
  }
}

State synchronization

Use SyncStateMixin to automatically sync state between server and client:
import 'package:jaspr/jaspr.dart';

class Counter extends StatefulComponent {
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> with SyncStateMixin {
  int count = 0;
  
  @override
  int getState() => count;
  
  @override
  void updateState(int value) {
    setState(() {
      count = value;
    });
  }
  
  @override
  Component build(BuildContext context) {
    return div([
      text('Count: $count'),
      button(
        onClick: (e) => setState(() => count++),
        [text('Increment')],
      ),
    ]);
  }
}
The initial count value is rendered on the server and automatically synced to the client during hydration.

Preloading data

Use PreloadStateMixin to fetch data on the server before rendering:
import 'package:jaspr/jaspr.dart';

class ProductPage extends StatefulComponent {
  final String productId;
  
  const ProductPage({required this.productId});
  
  @override
  State<ProductPage> createState() => _ProductPageState();
}

class _ProductPageState extends State<ProductPage> with PreloadStateMixin {
  late Product product;
  
  @override
  Future<void> preload() async {
    // Runs on the server before rendering
    product = await fetchProduct(component.productId);
  }
  
  @override
  Component build(BuildContext context) {
    return div([
      h1([text(product.name)]),
      p([text(product.description)]),
      p([text('\$${product.price}')]),
    ]);
  }
}

Configuration

Configure SSR in jaspr_options.yaml:
mode: server

servers:
  - name: default
    target: bin/server.dart
    port: 8080

clients:
  - name: web
    target: web/main.dart

Port configuration

You can also set the port in pubspec.yaml:
jaspr:
  server:
    port: 3000

Running your SSR app

Development

jaspr serve
This starts the development server with:
  • Hot reload for both server and client code
  • Live browser refresh
  • Debug mode compilation

Production build

jaspr build
This creates:
  • Optimized server executable in build/jaspr/bin/server.exe
  • Minified client JavaScript in build/jaspr/web/

Running in production

./build/jaspr/bin/server.exe

Advanced server setup

For more control, use serveApp() instead of runApp():
import 'package:jaspr/server.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:my_ssr_app/app.dart';

void main() async {
  final handler = serveApp(
    (request, render) {
      return render(
        Document(
          title: 'My SSR App',
          body: App(),
        ),
      );
    },
  );
  
  // Add custom middleware
  final pipeline = const Pipeline()
      .addMiddleware(logRequests())
      .addHandler(handler);
  
  // Start server
  await shelf_io.serve(pipeline, 'localhost', 8080);
  print('Server running on http://localhost:8080');
}

SEO optimization

Add meta tags dynamically based on content:
import 'package:jaspr/jaspr.dart';

class BlogPost extends AsyncStatelessComponent {
  final String slug;
  
  const BlogPost({required this.slug});
  
  @override
  Future<Component> build(BuildContext context) async {
    final post = await fetchBlogPost(slug);
    
    return Document.head(
      title: post.title,
      meta: {
        'description': post.excerpt,
        'og:title': post.title,
        'og:description': post.excerpt,
        'og:image': post.coverImage,
        'og:type': 'article',
        'twitter:card': 'summary_large_image',
      },
      child: div([
        h1([text(post.title)]),
        RawText(post.htmlContent),
      ]),
    );
  }
}

Best practices

Keep server-side rendering fast by:
  • Caching database queries
  • Using async components efficiently
  • Avoiding expensive computations in build()
Always handle errors in async operations:
@override
Future<Component> build(BuildContext context) async {
  try {
    final data = await fetchData();
    return DataView(data: data);
  } catch (e) {
    return ErrorView(error: e);
  }
}
  • Use SSR for content-heavy pages that need SEO
  • Use CSR for admin dashboards and internal tools
  • Use SSG for static content like blogs and docs

Next steps

Rendering modes

Learn about all rendering modes

State synchronization

Deep dive into SyncStateMixin

Preloading data

Master PreloadStateMixin

Deploy your app

Deploy to production

Build docs developers (and LLMs) love