Picnic logo

Picnic’s Page Platform from a Mobile perspective: enabling fast updates through server-driven UI

Written by Lucas TwiskJan 30, 2025 08:2634 min read
2500 blogposts images 03

After introducing our Page Architecture initiative in this previous post, we’ll now dive deeper into how we transformed the mobile app — the primary platform where millions of customers do their grocery shopping with Picnic. As an online-only supermarket, the app isn’t just another sales channel — it’s the core of all customer experience.

This transformation isn’t just about technical improvements — it’s about fundamentally changing how we deliver rich, dynamic user interfaces to customers.

When doing traditional app development for content-heavy apps like Picnic’s, you’ll notice that the implementation of screens follow a consistent pattern:

  • Fetch content from backend services
  • Convert the raw API responses into typed data structures
  • Transform these data structures into UI models with presentation logic
  • Define layouts by composing components from our design system and mapping the data to them

While this pattern served us well initially, it came with significant challenges. Feature development was becoming predictable but not necessarily efficient. Each new screen or feature required an app update, which meant significant delays in reaching all customers. Even minor changes, like adjusting a corner radius in the design system, required pushing a new app version through the app stores. Our data shows that it typically takes 2–3 weeks for 80% of customers to adopt a new app version. The remaining users, particularly those who have opted out of automatic updates, only update their app when we enforce a minimum version requirement — something we limit to once per year to avoid disrupting their experience. This created a frustrating situation where simple design tweaks, bug fixes and new features could take weeks or even months to reach all users, significantly slowing down our ability to iterate and improve the customer experience.

At Picnic, the ability to release updates quickly is crucial to our operations for several reasons:

First, as a fast-growing scale-up, we need to move at a high pace. When we develop new features or functionality, we want them available to all customers as quickly as possible to maintain our growth momentum and competitive edge.

Second, we strongly believe in continuous delivery and experimentation. As a company providing an essential service, quality is paramount — yet building software inevitably carries risks. Shipping smaller, incremental changes lets us maintain rigorous quality control while enabling quick detection and rollback of issues. This approach minimizes the impact of any problems on customers while maximizing engineering time spent on delivering value rather than extensive pre-release testing. However, the traditional app update cycle was holding us back from achieving this agility.

The transition to React Native was an important first step in simplifying our development process. Instead of maintaining separate implementations for iOS and Android, we could now build features once and deploy them across both platforms. This significantly reduced development overhead and helped maintain consistency. However, we still faced the fundamental challenge of slow, pull-based app update cycles.

Rather than making incremental improvements, we decided to reimagine our approach entirely. We set several ambitious goals:

  1. Enable shipping features without requiring customers to update the app
  2. Maintain exceptional user experience
  3. Preserve developer experience and testability
  4. Enable non-technical colleagues to make changes to the app

Introducing Server-Driven UI

We quickly realised that, to tick all the boxes, we would have to orchestrate rendering from the backend, so we started looking into server-driven UI (SDUI) approaches.

Implementing SDUI requires determining the right level of abstraction — there’s no universal solution, and the choice largely depends on specific use cases.

A common strategy is to use a High-Level Section-Based approach, where the server sends which pre-defined larger UI blocks to render, but the actual implementation of these blocks lives on the client.
This is the path taken by companies like Airbnb, and it offers several advantages such as a simple API structure and compact payloads. It also makes implementing custom animations and performance optimisations easier, because most UI customization and performance-critical rendering logic remains on the frontend, where it can be highly optimized for each platform.

However, this approach still requires app updates when introducing new sections or making significant changes to existing ones. Instead of sending which sections to render, we decided to implement a system in which the server sends detailed UI rendering instructions to the client. We define the interface at the most basic UI element level (think buttons, text, images, etc.) using a structured format that describes exactly how each element should be rendered. For example, instead of just saying “render a product section”, our server might send instructions like “render an image at these coordinates, with this text below it, using this font size and color, and wrap it in a button with these hover effects.” This granular level of control provides several advantages:

  1. Immediate UI changes: UI changes deploy without app updates. This applies to everything from minor styling adjustments to complete screen redesigns or even new features.
  2. Simple Rendering Engine: Operating with a defined set of atomic UI elements keeps the client-side rendering engine compact and maintainable.
  3. API Stability: The implementation of basic building blocks results in a stable API. This stability reduces feature release complexity and minimizes backwards compatibility concerns.

While the server sends atomic-level rendering instructions, we don’t compose pages directly from these primitive elements. To maintain consistency and prevent duplication, we’ve implemented a three-tiered component hierarchy on the server:

  1. Core Components
    – 
    Foundation layer containing atomic UI elements
    – Highly reusable and consistently styled
    – Examples: product tiles, recipe tiles, Picnic-styled buttons
  2. Section Components
    – 
    Mid-level building blocks composed of core components
    – Encapsulate a combination of core components and interactions
    – Examples: targeted product tile sections or a list of product categories
  3. Layout Components
    – 
    Top-level compositions of section components
    – Define the overall structure and flow of key screens
    – Examples: home page, meals planner
An example of how our home screen is built up out of different components

This hierarchical approach enables our teams to work at the appropriate level of abstraction while ensuring consistency across the app. Core components and section components are primarily built and maintained by engineers, ensuring performance, accessibility, and reusability. Layout components, however, can be created and modified by analysts and store operators using these pre-built sections, allowing non-technical teams to iterate on layouts and content without requiring engineering capacity.

To implement this hierarchy, each component on the server defines a `context` and a `layout`. The context defines the data the component will present, this can either be provided from above (params) or by executing a query. The layout defines how the component presents the data. In the layout, data points can be resolved with the $-syntax. Let’s see how this works by examining the card component (highlighted at the bottom in red in the image above), which is simply defining the layout for the image, title and subtitle (please note this is simplified pseudocode):

# Core: Card Templatename: cardcontext:  params:    image: URL    title: String    subtitle: Stringlayout:  type: column  children:    - type: image      source: $image    - type: text      value: $title    - type: text      value: $subtitle

This card template is then used by the card section (highlighted in green) to create horizontal lists of cards:

# Section: Card Section Templatename: card_sectioncontext:  params:     items: [Item]layout:  type: list  direction: horizontal  children:    - for_each: $items      template: card      image: $.image      title: $.title      subtitle: $.subtitle

Finally, we compose our page layout by fetching promotion data and passing it to the card section:

# Layout: Page Templatename: pagecontext:  query:    promotions: SELECT image_url, title FROM promotions LIMIT 2layout:  type: column  children:    - template: card_section      items: $promotions

While these templates help us organize our UI logic on the server, the client receives only atomic components. Here’s what the actual response could look like:

# Example page response{  "type": "column",  "children": [    {      "type": "list",      "direction": "horizontal",      "children": [        {          "type": "column",          "children": [            {              "type": "image",              "source": "robijn.jpg"            },            {              "type": "text",              "value": "Robijn"            },            {              "type": "text",              "value": "Combineren mogelijk"            }          ]        },        {          "type": "column",          "children": [            {              "type": "image",              "source": "picnic-kaas.jpg"            },            {              "type": "text",              "value": "Picnic kaas"            },            {              "type": "text",              "value": "Bv. jong belegen"            }          ]        }      ]    }  ]}

This approach means our client-side rendering engine remains simple — it only needs to render basic elements like images and text, with no knowledge of rendering higher-level concepts like cards or sections.

When we started developing this system, React (Native) Server Components were not yet available. With their introduction and maturation, we’re currently exploring how we can leverage them to reduce the amount of custom code we need to maintain ourselves. The principles behind our SDUI approach align well with the server-first rendering paradigm that React Server Components enable, potentially offering a more standardized way to achieve similar benefits.

Platform Benefits

Our investment in creating a unified platform approach through Page Architecture has yielded significant advantages in handling cross-cutting concerns. Rather than implementing these crucial aspects separately for each feature, the standardized architecture provides consistent solutions across our entire application:

  • Navigation is elegantly simplified through a single system: each destination is a ‘page’ deeplink with parameters that are forwarded to the backend when fetching the next Page Architecture-driven page. This eliminated the need for complex route handlers and deep-linking configurations.
  • Analytics coverage is comprehensive and uniform. By implementing generic analytics events at the platform level, we automatically capture views and user actions across all features. This provides consistent visibility and insights across our entire application without requiring feature-specific instrumentation.
  • The standardized architecture enables unified monitoring and alerting in Datadog. Performance metrics, error tracking, and reliability monitoring now follow consistent patterns across all features instead of requiring feature-specific implementations.

This platform-centric approach demonstrates how investing in robust, unified architecture can dramatically simplify development while improving consistency and reliability across the application.

Challenges

Like any architectural decision, our approach to server-driven UI comes with its own set of challenges. While the flexibility of composing UIs from atomic elements offers significant advantages, it also introduces complex technical considerations that require careful solutions.

One of our primary challenges was maintaining smooth 60fps scrolling performance, particularly in long lists of items like product grids. In traditional app development, view recycling is a common pattern to optimize memory usage and rendering performance by reusing view instances instead of creating new ones during scrolling. However, this becomes significantly more complex when each list item is, from the app’s perspective, simply a composition of multiple atomic UI elements.

The challenge deepens when dealing with heterogeneous lists containing different types of items. View recycling is most effective when recycling items with similar structure and view hierarchy, as different hierarchies still require new view allocations. To address this, we attach metadata to each composition of atomic elements, indicating its structural type (for example, “product-tile” or “recipe-tile”). The rendering engine uses this metadata to maintain separate recycling pools for different component types. This means that when a product tile scrolls off screen, its views can be efficiently recycled for the next product tile that scrolls into view, even though both tiles are technically just compositions of atomic elements. This system preserves the flexibility of atomic composition while maintaining the performance benefits of view recycling.

Another significant challenge we encountered was implementing custom animations and transitions. While our atomic UI approach excels at static layouts, complex animations often require more fine-grained control over the rendering process. Our ultimate goal is to express all UI elements, including complex animations, through our server-driven model. However, to accelerate our migration and maintain product quality, we implemented a pragmatic interim solution: the ability to fall back to components bundled with the app. Similar to how React Native allows exposing native components to JavaScript, our server-driven UI system can reference pre-built components shipped with the client.

This hybrid approach allowed us to maintain the custom animations of our product tile — one of the most complex components in our app — using existing client-side logic while progressively moving other page elements to server-driven components.

The animating quantity stepper is shipped inside the client, while the rest of the product tile is defined on the server side

Looking Back and Ahead

When we started this journey, we set out to fundamentally change how we deliver features to customers. Our Page Architecture approach has delivered on its initial promises and more. What previously took days — shipping new features or rolling back problematic changes — now happens in seconds. Building features has become even faster thanks to the platform benefits: teams get analytics, monitoring, and observability out of the box, letting them focus on delivering value rather than implementing infrastructure.

Most importantly, we’ve achieved this transformation without compromising on quality. Today, the majority of screens are built using Page Architecture, while maintaining our high standards for performance and user experience. Our metrics for scroll performance, load times, and error rates have remained strong, showing that we can have both flexibility and reliability.

The success of this platform approach has changed how we think about mobile development at Picnic. Rather than seeing our app as a collection of individual features, we now have a unified system that enables rapid iteration while maintaining consistency and quality. It’s not just about shipping faster — it’s about shipping with confidence.

Stay tuned for upcoming blog posts in this series where we’ll explore how we handle interactive pages beyond static content, and dive into our developer environment that enables any technically-inclined Picnic employee to build, test, and deploy app changes directly.

This article is part of a series of blogposts on the architecture behind our grocery shopping app. Hungry for more? Chew off another bite with any of our other articles:

picnic blog job banner image

Want to join Lucas Twisk in finding solutions to interesting problems?