Hyperverse

Faire du web depuis R, le web comme il a été conçu.

Arthur Bréant

2026-06-17

Prologue

“Ce que tu fais avec Shiny, ce n’est pas du vrai web.”

Mon ancien patron

Quelques années plus tard…

Il n’avait pas complètement tort.

Mais pas complètement raison non plus.

Le déclic

La release de {plumber2} sur le CRAN.


🔥 Un serveur HTTP moderne pour R : routes, async, sérialisation, …

Faire du web depuis R, le web comme il a été conçu.

hyperverse

hyperverse

Un écosystème de packages R pour construire des apps web modernes, à la manière du web.


🔥 Pas un remplacement de Shiny, juste une autre approche.

La stack en un coup d’œil

📦 htmxr → interactivité serveur via HTTP

📦 alpiner → interactivité client pure

📦 htmxr.bootstrap → composants stylés

📦 glitchtipr → monitoring d’erreurs


Aujourd’hui, focus sur htmxr : la pièce centrale.

htmxr

Le pont entre htmx (lib JS, attributs HTML) et plumber2 (serveur HTTP R).


🔥 Tu écris du R : htmxr génère du HTML avec les attributs htmx que le navigateur sait lire.

Les deux mécaniques

Comment Shiny communique

%%{init: {
  "theme": "base",
  "themeVariables": {
    "fontFamily": "Lato, system-ui, sans-serif",
    "background": "#f8f8f8",
    "primaryColor": "#1a1a1a",
    "primaryTextColor": "#f8f8f8",
    "primaryBorderColor": "#1a1a1a",
    "lineColor": "#2d2d2d",
    "actorBkg": "#1a1a1a",
    "actorBorder": "#1a1a1a",
    "actorTextColor": "#f8f8f8",
    "actorLineColor": "#2d2d2d",
    "signalColor": "#2d2d2d",
    "signalTextColor": "#2d2d2d",
    "noteBkgColor": "#ffffff",
    "noteTextColor": "#2d2d2d",
    "noteBorderColor": "#4a6cf7",
    "sequenceNumberColor": "#f8f8f8"
  }
}}%%
sequenceDiagram
    autonumber
    participant N as Navigateur
    participant S as Serveur R (Shiny)
    Note over N,S: WebSocket persistant
    Note over S: Session R dédiée
    N->>+S: input$bins = X
    Note right of S: invalide les dépendances<br/>recalcule ce qu'il faut
    S-->>-N: output$plot = <PNG>
    Note over N: Shiny met à jour l'élément<br/>lié à output$plot

🔥 Le serveur garde le tuyau ouvert.

Comment htmxr communique

%%{init: {
  "theme": "base",
  "themeVariables": {
    "fontFamily": "Lato, system-ui, sans-serif",
    "background": "#f8f8f8",
    "primaryColor": "#1a1a1a",
    "primaryTextColor": "#f8f8f8",
    "primaryBorderColor": "#1a1a1a",
    "lineColor": "#2d2d2d",
    "actorBkg": "#1a1a1a",
    "actorBorder": "#1a1a1a",
    "actorTextColor": "#f8f8f8",
    "actorLineColor": "#2d2d2d",
    "signalColor": "#2d2d2d",
    "signalTextColor": "#2d2d2d",
    "noteBkgColor": "#ffffff",
    "noteTextColor": "#2d2d2d",
    "noteBorderColor": "#4a6cf7",
    "sequenceNumberColor": "#f8f8f8"
  }
}}%%
sequenceDiagram
    autonumber
    participant N as Navigateur
    participant S as Serveur R (plumber2 + htmxr)
    N->>+S: GET /plot?bins=X
    Note right of S: exécute la route<br/>génère le HTML
    S-->>-N: <svg>...</svg>
    Note over N: htmx swap le SVG dans #35;plot

🔥 Le navigateur demande, R répond. Pas de tuyau, pas de session R.

Une phrase à retenir

En Shiny, le serveur réagit,

En htmxr, le navigateur demande.

La démo

🔗 hello.demo.hyperverse.world

Plutôt rapide, hein ? 😏

Le banc d’essai

htmxr

──────────────────────

🔗 hello.demo.hyperverse.world

Shiny via Shinyproxy

─────────────────────

🔗 open-apps.breant.art/app/hello-shiny


🔥 Même VPS OVH, même hardware, pour rendre la comparaison juste.


Pour info, l’app Shiny tourne aussi sur Posit Connect : 🔗 connect.thinkr.fr/hello-shiny (infra différente, hors chrono).

Chargement initial

Temps d’attente avant que le plot soit visible dans le DOM.

htmxr

──────────────────────

🔥 0.3s

Shiny via Shinyproxy

─────────────────────

🔥 0.9s

Interaction slider

Ce qui se passe quand on bouge le slider, étape par étape.

htmxr

──────────────────────

🔥 Quasi instantané

  • GET /plot?bins=X
  • R génère un SVG en ~12 ms
  • htmx swap dans le DOM

→ Trois étapes, toutes standard.

Shiny via Shinyproxy

─────────────────────

🔥 Micro délai

  • nouvelle valeur via WebSocket
  • invalidation du graphe réactif
  • recalcul
  • rendu PNG, encodage base64
  • retour via WS
  • update DOM

→ Sept étapes, dont plusieurs spécifiques à Shiny.

Poids client

Pourquoi ces différences ?

Deps JS/CSS : 2 vs 13


📦 htmxr : 30 lignes de HTML, 2 deps (htmx.js + Bootstrap).


📦 Shiny : 48 lignes, 13 deps (jQuery, shiny.js, ionRangeSlider, bs3compat, …).

rapide et économique, vraiment ? 🤔

#1 HTTP est stateless par design

htmxr (stateless HTTP) ──────────────────────

1 process R partagé

RAM ♾️ requêtes actives

User inactif = gratuit

Shiny (stateful WS) ─────────────────────

1 session R par user

RAM ♾️ users connectés

User inactif = coûte


🔥 On paie le travail réel, pas les sessions inactives.

#2 La facture OVH

À hardware égal (VPS-1 OVH, 6.62 € TTC/mois) :

Architecture Capacité
htmxr 1 process partagé 1000+ users
Shiny via Shinyproxy 1 container / user ~20 users


À charge égale (500 utilisateurs actifs) :

Infrastructure Coût
htmxr VPS-1 OVH 6.62 € TTC/mois
Shiny via Shinyproxy dédié RISE-1 64 GB ~78 € HT/mois

🔥 ~12× sur la facture, pour la même charge utilisateur.

#3 Interopérabilité : une seule API

🔗 hello.demo.hyperverse.world/plot?bins=20

→ Cette URL renvoie directement le SVG.

Browser, curl, Python, autre app R, webhook. N’importe quel client HTTP.


Le pattern : même donnée, deux sérialiseurs.

#* @get /api/rows                  #* @get /rows
#* @serializer json                #* @serializer html
function() diamond_data()          function() hx_table_rows(diamond_data())
#  ← curl, Python, JS              #  ← navigateur via htmx

🔥 Même backend, deux clients.


❌ Avec Shiny, une app n’est consommable que par un navigateur qui fait tourner Shiny.

#4 Ce qu’on hérite gratuitement

Cache HTTP, CDN, replicas

Scaling horizontal (stateless oblige)

Monitoring standard (codes HTTP, logs nginx, métriques)

URLs adressables (partage par mail)

✅ Déployable partout (Docker, VPS, k8s, serverless)

Debugging via les DevTools du navigateur


🔥 L’expérience du web.

Côté code

Un slider

htmxr

hx_slider_input(
  id      = "bins",
  label   = "Number of bins:",
  min     = 1,
  max     = 50,
  value   = 30,
  get     = "/plot",
  target  = "#plot",
  trigger = "input changed delay:300ms"
)

Shiny

sliderInput(
  inputId = "bins",
  label   = "Number of bins:",
  min     = 1,
  max     = 50,
  value   = 30
)


💡 Mapping mental préservé : mêmes paramètres “standards” + 3 attributs pour câbler la requête HTTP.

La structure d’une app

htmxr (api.R)

#* @get /
#* @serializer html
function() {
  hx_page(
    hx_slider_input("bins", ..., get = "/plot"),
    tags$div(id = "plot")
  )
}

#* @get /plot
#* @serializer none
function(query) {
  hist_svg(bins = as.integer(query$bins))
}

Shiny (app.R)

ui <- fluidPage(
  sliderInput("bins", ...),
  plotOutput("plot")
)

server <- function(input, output) {
  output$plot <- renderPlot({
    hist(x, breaks = input$bins)
  })
}

shinyApp(ui, server)


💡 Deux types de routes : la page complète et les fragments HTML.

Et reactiveValues ?

htmxr

# /viz?cut=...

#* @get /viz
function(query, datastore) {
  datastore$session$cut <- query$cut
  render_viz(query$cut)
}

🍪 datastore$session

🌍 datastore$global

Shiny

r_global <- reactiveValues()

# r_global$cut <- ...

observeEvent(r_global$cut, {
  update_viz(r_global$cut)
})


💡 Pas de blackboard réactif : chaque concern à sa place.

Tout n’existe pas (encore)

🚧 Pas d’équivalent golem / rhino pour structurer une grosse app.

🚧 htmxr.bootstrap encore en dev (theming Sass instable).

🚧 glitchtipr / alpiner pas encore publié sur le CRAN.

🚧 Pas encore de retour de prod massive : à éprouver.


🔥 L’écosystème est jeune → appel à contributions ouvert.

À qui s’adresse htmxr ?

htmxr brille pour

  • Apps publiques avec beaucoup d’utilisateurs
  • Apps avec contraintes de rapidité (même en interne)
  • Budgets serrés (~12× moins cher en infra)
  • Endpoints réutilisables : web, mobile, autres services
  • Devs R à l’aise avec HTTP, ou curieux du web

Shiny reste le bon choix pour

  • Dashboards internes, petite audience
  • Apps qui dépendent de DT, plotly, bslib…
  • Réactivité fine-grained nécessaire
  • Stack en place, deadline serrée


🔥 Deux outils, pas un duel.

hyperverse

📖 Doc → 🔗 hyperverse.world


🚀 Démo live → 🔗 hello.demo.hyperverse.world


💻 GitHub → 🔗 github.com/hyperverse-r

Merci !