Performances Shiny : cache, profilage et asynchrone avec bindCache et profvis

Trouvez le vrai goulot d’étranglement avec profvis, puis rendez rapide une application lente : mettez en cache avec bindCache, déclenchez avec bindEvent, domptez les entrées rapides avec debounce, et sortez le travail long du processus principal avec promises et future.

Programming
Shiny

Rendez rapide une application Shiny lente. Comprenez pourquoi un unique processus R se retrouve débordé, profilez avec profvis pour trouver le véritable goulot d’étranglement, mettez en cache le travail coûteux avec bindCache et renderPlot |> bindCache, ne calculez que sur un déclencheur avec bindEvent, regroupez les entrées rapides d’un curseur ou de la frappe avec debounce et throttle, gérez les grandes données sans les copier dans chaque réactif, et utilisez promises avec future pour que les tâches longues ne bloquent pas les sessions des autres utilisateurs.

Date de publication

16 juillet 2026

Modifié

16 juillet 2026

AstucePoints clés
  • Une application, c’est un seul processus R, qui n’exécute qu’une chose à la fois. Un calcul lent ne fait pas que faire attendre cet utilisateur — il fige l’application pour toutes les personnes connectées jusqu’à ce qu’il se termine. La vitesse vient du fait de faire le travail coûteux moins souvent, et de sortir le travail long de cet unique processus.
  • Profilez avant d’optimiser. profvis vous montre quelle ligne est réellement lente — rarement celle que vous auriez devinée. Corrigez la barre la plus large du flame graph, pas votre intuition.
  • Mettez en cache le travail coûteux avec bindCache(). Enveloppez un réactif ou un renderPlot() et listez les entrées dont il dépend comme clé de cache ; la deuxième requête pour les mêmes entrées répond instantanément, même si elle vient d’un autre utilisateur.
  • Ne calculez que lorsque vous le voulez vraiment. bindEvent() ne recalcule que sur un déclencheur choisi (un bouton Run) ; debounce() et throttle() regroupent le déplacement d’un curseur ou des frappes en un seul calcul au lieu de dizaines.
  • L’asynchrone achète de la concurrence, pas de la vitesse pour un utilisateur seul. promises + future exécutent une tâche longue dans un processus en arrière-plan pour que les autres sessions restent réactives — mais la tâche elle-même prend exactement le même temps. Copiez n’importe quel bloc et exécutez-le localement : the runtime is the judge.

Introduction

Votre tableau de bord saccade. Chaque fois que l’utilisateur pousse le curseur des années, toute la page se bloque pendant deux secondes le temps qu’un graphique se redessine — et si une deuxième personne utilise l’application, elle est figée elle aussi. C’était rapide sur votre ordinateur portable avec les données de test ; avec les vraies données et de vrais utilisateurs, ça traîne.

Cet écart tient presque toujours aux deux mêmes problèmes. D’abord, une application Shiny est un unique processus R, et R n’exécute qu’une chose à la fois — donc un calcul lent bloque chaque utilisateur, pas seulement celui qui l’a déclenché. Ensuite, Shiny est réactif : quand une entrée change, tout ce qui en dépend en aval se réexécute, et il est facile de relier un calcul coûteux à une entrée qui change constamment, si bien que le travail lent s’exécute des dizaines de fois là où il n’aurait dû s’exécuter qu’une seule fois.

Tout dans cette leçon découle de ces deux faits. Vous faites en sorte que le travail coûteux se produise moins souvent — mettez-le en cache, déclenchez-le délibérément, regroupez les entrées rapides — et quand il prend réellement beaucoup de temps, vous le sortez du processus principal pour qu’il cesse de bloquer tout le monde. Mais d’abord, vous mesurez, car la ligne lente est rarement celle que vous supposez. Voici le guide de performance tiré du chapitre performance de Mastering Shiny, dans l’ordre où vous l’appliquez réellement.

NotePrérequis

Il vous faudra une application Shiny fonctionnelle qui soit réellement lente — les outils ci-dessous sont des remèdes, pas des vitamines. Installez les paquets de profilage et d’asynchrone que cette leçon utilise :

install.packages(c("profvis", "promises", "future"))

bindCache(), bindEvent(), debounce() et throttle() sont livrés avec Shiny lui-même (version 1.6 et ultérieures), donc dès que vous avez Shiny vous les avez déjà. Les fondations de la programmation réactive expliquent le graphe réactif que cette leçon optimise, et tests et débogage couvre reactlog, l’outil complémentaire pour voir pourquoi quelque chose se réexécute.

NoteCopiez n’importe quel bloc et exécutez-le en local

Le code ci-dessous est de la logique server Shiny — il a besoin d’une session R active et d’une application en fonctionnement, ce qu’une page web statique ne peut pas héberger. Rien ne s’exécute donc sur cette page : copiez n’importe quel bloc dans une vraie application et exécutez-le localement. La preuve est dans l’exécution — profvis ouvre un flame graph de votre application, bindCache() rend le deuxième clic instantané, et un future en arrière-plan garde l’application réactive pendant qu’il travaille. The runtime is the judge.

Pourquoi Shiny devient lent

Avant tout correctif, gardez en tête le bon modèle mental — c’est lui qui vous dit quel correctif choisir.

Une application Shiny, c’est un seul processus R. Quand vous déployez une application, chaque instance en fonctionnement est une unique session R, et R est mono-thread : il exécute une expression à la fois. Chaque réactif, chaque render, chaque calcul pour chaque utilisateur connecté à cette instance attend son tour sur cet unique processus. Ainsi, un calcul qui prend cinq secondes ne coûte pas cinq secondes à un utilisateur — il fige l’application entière pour tous pendant cinq secondes, car rien d’autre ne peut s’exécuter tant qu’il n’a pas rendu la main.

Shiny recalcule de façon réactive. Quand une entrée change, Shiny réexécute chaque réactif et chaque sortie qui en dépend. C’est tout l’intérêt de la réactivité — mais c’est aussi là que se cache la lenteur. Le cas classique est la sur-réactivité : un réactif coûteux relié à une entrée qui change rapidement. Un curseur se déclenche à chaque pixel de déplacement ; une zone de texte à chaque frappe. Si un calcul de deux secondes dépend de ce curseur, le déplacer une fois peut mettre ce calcul en file d’attente dix fois.

Deux leviers en découlent directement, et ils organisent le reste de cette leçon :

Le problème Le levier Les outils
Le travail coûteux s’exécute plus souvent que nécessaire Le faire moins souvent bindCache(), bindEvent(), debounce(), throttle()
Une tâche vraiment longue bloque tous les autres utilisateurs Le sortir du processus principal promises + future

Et avant l’un ou l’autre : découvrez ce qui est réellement lent.

Profilez d’abord — mesurez, ne devinez pas

L’erreur de performance la plus courante est d’optimiser la mauvaise chose. Vous pensez que le graphique est lent ; c’est en fait le chargement des données qui l’alimente. profvis élimine les devinettes en échantillonnant votre code pendant qu’il s’exécute et en dessinant un flame graph — une image de l’endroit où le temps passe réellement.

Profilez un morceau de code en l’enveloppant dans profvis() :

library(profvis)

profvis({
  data  <- read_and_clean("big.csv")   # is the time here…
  model <- fit_model(data)             # …or here?
})

Le flame graph qui s’ouvre se lit simplement : la largeur, c’est le temps. La barre la plus large est votre goulot d’étranglement. Si read_and_clean() représente 90 % de la largeur, mettre le modèle en cache n’y changera rien — vous corrigez le chargement des données. profvis répond à « qu’est-ce qui est lent » par des preuves plutôt que par une intuition.

Pour profiler une application entière, passez runApp() à profvis(), interagissez avec l’application, puis fermez-la :

profvis(runApp(shinyApp(ui, server)))
# use the app for a bit, close it, and the flame graph opens

Cliquez maintenant un peu partout — déplacez le curseur, changez les entrées — et quand vous vous arrêtez, le flame graph montre exactement quels réactifs et quelles fonctions ont consommé le temps au cours de cette session.

profvis vous dit ce qui est lent. Son compagnon, reactlog, vous dit ce qui s’est réexécuté et pourquoi — le problème de sur-réactivité ci-dessus apparaît comme un réactif qui se déclenche bien plus souvent que prévu. Les deux ensemble diagnostiquent presque tout ralentissement Shiny ; reactlog est traité dans tests et débogage. Mesurez avec les deux, puis choisissez un correctif ci-dessous.

Mettez en cache le travail coûteux avec bindCache()

Une fois que profvis a pointé un calcul coûteux, le correctif au plus fort effet de levier est d’arrêter de le répéter. Si les mêmes entrées produisent le même résultat, calculez-le une fois et retenez-le. bindCache() — l’outil de mise en cache moderne, présent dans Shiny depuis la 1.6 — fait exactement cela : enveloppez un réactif, listez les entrées dont il dépend comme clé de cache, et Shiny stocke chaque résultat sous sa clé.

server <- function(input, output, session) {

  # Expensive: a heavy aggregation over a chosen year and region
  summary <- reactive({
    heavy_aggregate(sales, input$year, input$region)
  }) |> bindCache(input$year, input$region)

  output$table <- renderTable(summary())
}

La première fois qu’une paire (year, region) est demandée, le corps s’exécute et le résultat est stocké sous cette clé. Toute demande ultérieure de la même paire renvoie instantanément la valeur stockée — aucun recalcul. Revenez à une année que vous avez déjà consultée, et c’est immédiat. La référence de bindCache() présente l’API complète.

Deux règles rendent la mise en cache correcte plutôt que dangereuse :

  • La clé doit inclure chaque entrée que le corps lit. Si le corps utilise input$year, input$region et input$metric, les trois figurent dans la clé. Oubliez-en une et Shiny vous renverra un résultat calculé pour un autre metric — une mauvaise réponse silencieuse, le pire type de bug.
  • Sachez que le cache est partagé entre les utilisateurs par défaut (cache = "app"). Un résultat calculé par un utilisateur est réutilisé pour tous — excellent pour des résultats coûteux qui sont identiques pour un jeu d’entrées donné, mais ne mettez jamais en cache à l’échelle de l’application des données propres à un utilisateur. Pour cela, limitez-en la portée par session avec cache = "session".

Mettre un graphique en cache fonctionne de la même façon — chaînez bindCache() sur le render :

output$plot <- renderPlot({
  ggplot(filtered(), aes(wt, mpg)) + geom_point()
}) |> bindCache(input$year, input$region)

bindCache() sur renderPlot() supplante l’ancienne fonction renderCachedPlot() — même idée, un seul outil cohérent aussi bien pour les réactifs que pour les sorties. Si vous croisez renderCachedPlot() dans du code ancien, lisez-le comme « un graphique mis en cache » ; le code récent devrait préférer renderPlot(...) |> bindCache(...).

Ne calculez que sur un déclencheur avec bindEvent()

La mise en cache aide quand les mêmes entrées reviennent. Mais parfois le correctif consiste à ne pas calculer du tout tant que l’utilisateur n’est pas prêt. Imaginez un formulaire avec trois entrées alimentant un modèle lent : par défaut, le modèle se recalcule dès que l’une des trois change, si bien que préparer une seule exécution vous coûte trois calculs. bindEvent() lie plutôt le calcul à un déclencheur explicite :

server <- function(input, output, session) {

  result <- reactive({
    run_expensive_model(input$dataset, input$params, input$method)
  }) |> bindEvent(input$run)   # recompute ONLY when the Run button is clicked

  output$out <- renderTable(result())
}

L’utilisateur règle maintenant dataset, params et method dans n’importe quel ordre, sans rien changer en aval, et ne paie qu’un seul calcul lorsqu’il clique sur Run. C’est la forme moderne d’eventReactive() ; chaînez bindEvent() sur un observe() et c’est le observeEvent() moderne. Il généralise les deux en un seul outil — voir la référence de bindEvent(). Utilisez-le dès qu’un réactif coûteux dépend d’entrées que l’utilisateur ajuste plusieurs fois avant de vouloir réellement un résultat.

Regroupez les entrées rapides avec debounce() et throttle()

Le déclencheur de la sur-réactivité est souvent un contrôle qui se déclenche en continu : un curseur émet une nouvelle valeur à chaque pixel de déplacement, une zone de recherche à chaque frappe. Vous ne voulez pas recalculer à chacune d’elles — vous voulez attendre que l’entrée se stabilise. debounce() et throttle() font cela, et ce sont des outils différents :

  • debounce(r, millis) attend que le réactif soit resté silencieux pendant millis millisecondes, puis émet sa dernière valeur. Parfait pour une zone de recherche (calculer une fois que l’utilisateur cesse de taper) ou un curseur qui ne vous intéresse qu’une fois qu’il est relâché.
  • throttle(r, millis) émet au plus une fois toutes les millis millisecondes pendant que la valeur continue de changer — une cadence régulière durant un déplacement, plutôt qu’une seule mise à jour à la fin.

Les deux enveloppent un réactif et renvoient un nouveau réactif, vous faites donc passer l’entrée à travers eux :

server <- function(input, output, session) {

  # input$term fires on every keystroke — wait for a 500 ms pause, then search
  term <- reactive(input$term) |> debounce(500)

  output$results <- renderTable({
    search_catalog(term())
  })
}

La recherche s’exécute désormais une fois que l’utilisateur fait une pause, pas à chaque lettre. Notez la forme : vous appliquez debounce() à un réactif, pas au input$term brut — enveloppez d’abord l’entrée dans reactive(). La référence de debounce()/throttle() couvre les détails de temporisation. Utilisez debounce() quand seule la valeur finale vous intéresse, throttle() quand vous voulez que l’affichage suive pendant le changement.

Gérez les grandes données sans les copier

La pression sur la mémoire est l’autre moitié de « lent » — et ici le correctif consiste surtout à ne pas faire de choses inutiles, plutôt qu’à recourir à des astuces ingénieuses.

Appuyez-vous sur la mise en cache réactive que vous avez déjà. Un reactive() mémoïse : il stocke son résultat et remet la même valeur à chaque appelant jusqu’à ce que ses entrées changent. Le correctif pour « le même filtre s’exécute dans cinq sorties différentes » est donc de le calculer une seule fois dans un réactif et de le faire lire par les cinq :

server <- function(input, output, session) {

  # Compute the filtered data ONCE; every output shares this result
  filtered <- reactive({
    subset(sales, year == input$year & region == input$region)
  })

  output$plot  <- renderPlot(ggplot(filtered(), aes(wt, mpg)) + geom_point())
  output$table <- renderTable(head(filtered()))
  output$n     <- renderText(nrow(filtered()))
}

Empilez-les — brut → filtré → résumé — pour que chaque étape ne se recalcule que lorsque ses entrées changent et que les étapes en aval réutilisent le résultat mis en cache. Cette structure à elle seule élimine la plupart du travail redondant.

Chargez les grandes données de référence une seule fois, pas par session. Les données en lecture seule dont chaque session a besoin — une table de correspondance, un shapefile, un modèle ajusté — ont leur place en haut de app.R ou dans global.R, à l’extérieur de la fonction server. Le code qui s’y trouve s’exécute une seule fois au démarrage de l’application et est partagé, en lecture seule, par toutes les sessions ; la même lecture à l’intérieur de server s’exécute une fois par utilisateur et multiplie la mémoire. (Voir organisation du code pour savoir où placer le code de démarrage.)

Repoussez le filtrage et l’agrégation vers la base de données. Les grandes données les moins coûteuses sont celles que vous ne chargez jamais. Si elles vivent dans une base de données, laissez la base faire le WHERE et le GROUP BY et ne renvoyer que les lignes dont vous avez besoin — un SELECT ... GROUP BY qui renvoie 200 lignes de synthèse vaut mieux que de tirer deux millions de lignes dans R et de les résumer là. La leçon connexions aux bases de données couvre le filtrage côté serveur et le paquet pool pour le faire en toute sécurité sous charge concurrente. Pour de grandes données qui doivent rester en mémoire, des paquets comme data.table, arrow et duckdb filtrent rapidement sans copier tout le tableau.

Ne luttez pas contre le ramasse-miettes. Vous verrez de vieux conseils recommandant de parsemer votre code d’appels à gc() partout pour « libérer de la mémoire ». Passez votre chemin — R récupère la mémoire tout seul, et un gc() manuel ne fait le plus souvent que bloquer votre unique processus (figeant chaque utilisateur) sans gain durable. R libère aussi automatiquement les objets d’une session quand celle-ci se termine. Ce qui a besoin d’un nettoyage explicite, ce sont les ressources externes — pools de base de données, descripteurs de fichiers, connexions — que vous libérez dans onStop() ou session$onSessionEnded(), pas avec gc().

Sortez le travail long du processus principal avec l’asynchrone

Certaines tâches sont vraiment lentes — un modèle qui prend 30 secondes, un appel d’API qui prend une minute — et aucune mise en cache ne rend le premier passage plus rapide. Ici le problème n’est pas la répétition, c’est le blocage : pendant que cette tâche de 30 secondes s’exécute sur l’unique processus R, tous les autres utilisateurs sont figés. Le traitement asynchrone déplace la tâche vers un processus R séparé pour que le principal reste libre.

Les paquets promises et future le fournissent. future::plan(multisession) met en place un pool de processus R en arrière-plan ; future({ ... }) exécute un bloc dans l’un d’eux ; et %...>% (le « promise pipe ») poursuit avec le résultat lorsqu’il est prêt :

library(promises)
library(future)
plan(multisession)          # a pool of background R processes

server <- function(input, output, session) {
  output$result <- renderTable({
    future({
      slow_model(input$params)    # runs in a background process
    }) %...>%
      format_result()             # continue here when it finishes
  })
}

Pendant que slow_model() s’exécute en arrière-plan, le processus principal est libre de servir tous les autres.

Soyez honnête sur ce que l’asynchrone vous apporte — et ce qu’il n’apporte pas. L’asynchrone ne rend pas votre calcul plus rapide. Le modèle de 30 secondes prend toujours 30 secondes ; l’utilisateur qui l’a lancé attend toujours 30 secondes son résultat. Ce qui change, c’est qui d’autre doit attendre : sans asynchrone, ce calcul fige toutes les autres sessions pendant 30 secondes ; avec lui, le travail lent s’exécute ailleurs et tous les autres restent réactifs. L’asynchrone achète de la concurrence et de la montée en charge, pas de la latence pour un utilisateur seul. Il a aussi des coûts réels — chaque future est un processus séparé dans lequel vos données doivent être copiées, si bien qu’envelopper une tâche rapide dans future() la rend plus lente, pas plus rapide. Ne l’utilisez que pour du travail vraiment long, sur une application qui sert plus d’un utilisateur. Ce compromis a des implications directes pour le déploiement en production : l’asynchrone n’aide que si votre hébergeur donne à l’application assez de processus de travail et de mémoire pour le pool d’arrière-plan.

Shiny moderne (1.8.1 et ultérieur) ajoute ExtendedTask, la primitive recommandée pour l’asynchrone à l’intérieur de Shiny — contrairement à une promise nue dans un render, elle garde aussi interactive la session du même utilisateur (il peut annuler, ou lancer une autre tâche) pendant que le travail s’exécute :

task <- ExtendedTask$new(function(params) {
  future_promise({ slow_model(params) })   # future_promise() is promises' Shiny-aware future()
})
# task$invoke(params) starts it; task$status() reports progress;
# task$result() reads the value when it's ready — all without blocking the session

L’article officiel sur l’asynchrone dans Shiny est la référence complète pour ExtendedTask et le modèle promises/future qui le sous-tend.

Problèmes fréquents

Votre cache renvoie un résultat périmé ou faux. Vous avez enveloppé un réactif dans bindCache() mais le corps lit une entrée que vous avez oublié de mettre dans la clé. Shiny indexe le résultat stocké uniquement sur les entrées que vous avez listées, donc quand l’entrée non listée change, il vous rend l’ancienne valeur. Listez chaque entrée que le corps lit comme clé de cache — si le corps utilise input$year, input$region et input$metric, les trois ont leur place dans bindCache(input$year, input$region, input$metric).

debounce() semble ne rien faire. Vous l’avez presque certainement appliqué à une entrée brute — input$term |> debounce(500) — au lieu d’un réactif. debounce() et throttle() opèrent sur un réactif et renvoient un réactif : enveloppez d’abord l’entrée, reactive(input$term) |> debounce(500), et lisez le réactif debounced (term()) en aval, pas input$term.

L’asynchrone a rendu votre application plus lente, ou rien ne s’exécute en parallèle. Deux causes. Soit vous avez enveloppé une tâche rapide dans future() — le coût de copie des données vers un processus en arrière-plan l’emporte sur un calcul déjà rapide, donc ne rendez asynchrone que le travail vraiment long. Soit vous n’avez jamais appelé plan(multisession), si bien que future() retombe sur une exécution séquentielle dans le processus principal (le plan(sequential) par défaut) et bloque exactement comme avant. Définissez le plan une fois, près du début de votre application.

Questions fréquentes

Procédez dans l’ordre. D’abord, profilez avec profvis pour trouver ce qui est réellement lent — c’est rarement ce que vous devineriez. Puis faites en sorte que le travail coûteux se produise moins souvent : mettez en cache les résultats répétés avec bindCache(), ne calculez que sur un déclencheur délibéré avec bindEvent(), et regroupez les entrées rapides d’un curseur ou de la frappe avec debounce()/throttle(). Évitez de copier les grandes données dans chaque réactif (calculez un filtre partagé une seule fois, chargez les données de référence dans global.R, ou filtrez dans la base de données). Enfin, pour les tâches vraiment longues, utilisez promises + future pour qu’elles ne bloquent pas les autres utilisateurs. Mesurez à nouveau après chaque changement.

bindCache() met en cache le résultat d’un réactif ou d’un render pour qu’il ne soit pas recalculé lorsque les mêmes entrées reviennent. Vous le chaînez sur un réactif et listez les entrées dont il dépend comme clé de cache : reactive({ heavy(input$x, input$y) }) |> bindCache(input$x, input$y). La première demande pour un (x, y) donné exécute le corps et stocke le résultat ; les demandes ultérieures pour la même paire le renvoient instantanément. Il supplante l’ancien renderCachedPlot() et fonctionne aussi sur renderPlot(). La règle unique : chaque entrée que le corps lit doit figurer dans la clé, sinon vous obtiendrez une valeur mise en cache erronée.

Les deux réduisent la fréquence à laquelle un réactif coûteux s’exécute, mais différemment. bindEvent() lie un calcul à un déclencheur explicite — il ne recalcule que lorsque, par exemple, un bouton Run est cliqué, en ignorant jusque-là les changements de ses autres entrées. debounce() (et throttle()) regroupent une entrée qui change rapidement — le déplacement d’un curseur ou des frappes — en moins de calculs en attendant que l’entrée se stabilise. Utilisez bindEvent() quand c’est l’utilisateur qui doit décider quand calculer ; utilisez debounce() quand l’entrée se déclenche en continu et que seule la valeur finale (ou throttled) vous intéresse.

Pas pour la personne qui attend le résultat. promises + future exécutent une tâche longue dans un processus R en arrière-plan, mais la tâche prend exactement le même temps — l’utilisateur qui l’a lancée attend toujours. Ce que l’asynchrone corrige, c’est le blocage : sans lui, un calcul lent fige chaque utilisateur connecté ; avec lui, le travail lent s’exécute ailleurs et les autres sessions restent réactives. L’asynchrone améliore donc la concurrence et la montée en charge, pas la vitesse pour un utilisateur seul, et il n’est payant que sur des tâches vraiment longues dans une application multi-utilisateur — les petites tâches sont plus lentes en asynchrone car les données doivent être copiées vers le processus en arrière-plan.

Enveloppez l’application dans profvis() : profvis(runApp(shinyApp(ui, server))). Interagissez avec l’application — déplacez les curseurs, changez les entrées — puis fermez-la, et profvis ouvre un flame graph où la largeur, c’est le temps, si bien que la barre la plus large est votre goulot d’étranglement. Optimisez celle-là, pas ce que vous supposez lent. profvis vous dit ce qui est lent ; associez-le à reactlog pour voir ce qui s’est réexécuté et pourquoi, ce qui est la manière dont la sur-réactivité (un réactif coûteux se déclenchant trop souvent) se révèle.

Testez vos connaissances

Vous avez un réactif lent qui dépend de trois entrées, et il se recalcule actuellement dès que l’une d’elles change :

server <- function(input, output, session) {
  result <- reactive({
    slow_summary(sales, input$year, input$region, input$metric)
  })
  output$table <- renderTable(result())
}

Retravaillez-le de deux façons : (1) pour qu’il ne se recalcule que lorsqu’un bouton Run (input$run) est cliqué, et (2) pour qu’un jeu d’entrées répété soit servi depuis le cache au lieu d’être recalculé.

Chaînez bindEvent(input$run) sur le réactif pour le conditionner au bouton, et chaînez bindCache(...) avec les trois entrées comme clé pour qu’une répétition des mêmes entrées soit mise en cache. Vous pouvez enchaîner les deux sur le même réactif.

server <- function(input, output, session) {
  result <- reactive({
    slow_summary(sales, input$year, input$region, input$metric)
  }) |>
    bindCache(input$year, input$region, input$metric) |>  # cache by ALL inputs the body reads
    bindEvent(input$run)                                   # recompute only on Run

  output$table <- renderTable(result())
}

bindEvent(input$run) signifie que rien ne se recalcule tant que l’utilisateur ne clique pas sur Run, si bien que régler les trois entrées coûte un calcul au lieu de trois. bindCache() stocke chaque résultat sous sa clé (year, region, metric), donc revenir à une combinaison que l’utilisateur a déjà exécutée est instantané. Notez que les trois entrées figurent dans la clé de cache — en oublier une servirait un résultat calculé pour une valeur différente de celle-ci.

Une application exécute un modèle de 40 secondes et se fige pour chaque utilisateur pendant ce temps. Vous enveloppez le modèle dans future() avec plan(multisession). Qu’est-ce que cela change ?

A. Le modèle se termine désormais en une fraction du temps pour l’utilisateur qui l’a lancé.
B. Le modèle prend toujours 40 secondes, mais il s’exécute dans un processus en arrière-plan pour que les sessions des autres utilisateurs restent réactives.
C. Le résultat du modèle est mis en cache, donc la deuxième exécution est instantanée.
D. Rien — future() n’a aucun effet à l’intérieur de Shiny.

B. L’asynchrone déplace la tâche longue vers un processus R séparé, si bien qu’elle ne bloque plus l’unique processus principal que partagent tous les autres utilisateurs — leurs sessions restent actives. Mais la tâche elle-même prend tout autant de temps : l’utilisateur qui l’a lancée attend toujours 40 secondes. A est faux (l’asynchrone achète de la concurrence, pas de la vitesse pour un utilisateur seul) ; C décrit bindCache(), pas future() ; D est faux — future() fonctionne bien dans Shiny, surtout via ExtendedTask, à condition d’avoir défini un plan() parallèle.

Conclusion

Une application Shiny rapide vient d’une habitude et d’une poignée d’outils. L’habitude, c’est mesurer d’abord : profvis vous dit quelle ligne est réellement lente, et reactlog vous dit ce qui se réexécute, pour que vous corrigiez le vrai goulot d’étranglement au lieu d’une supposition. Ensuite, vous faites en sorte que le travail coûteux se produise moins souvent — bindCache() pour réutiliser un résultat, bindEvent() pour ne calculer que sur un déclencheur délibéré, debounce()/throttle() pour regrouper les entrées rapides — et vous gardez les grandes données hors de chaque réactif en calculant les résultats partagés une seule fois, en chargeant les données de référence dans global.R, et en laissant la base de données faire le gros du filtrage. Quand une tâche est vraiment longue, promises et future la sortent de l’unique processus R pour qu’elle cesse de figer tout le monde — en se rappelant que l’asynchrone achète de la concurrence, pas de la vitesse pour un utilisateur seul. Profilez, supprimez le travail répété, puis parallélisez ce qui reste : cet ordre, c’est tout le guide.

Leçons connexes

Cette page vous a-t-elle été utile ?

Prouvez que vous savez le faire. Maîtrisez toute la série Performance Shiny — suivez votre parcours, construisez des projets et obtenez un certificat.

Commencer gratuitement →

Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.

dès 15 $/mois facturé annuellement

Passer à Pro →

✓ Vous êtes Pro — continuez. The runtime is the judge.

Recevez les nouvelles leçons R & Python par e-mail

Pratique, reproductible, sans spam. Désinscription à tout moment.

Double opt-in. Nous ne partageons jamais votre e-mail.

Partager cette pageXLinkedInRedditHN

Réutilisation

Citation

BibTeX
@online{2026,
  author = {},
  title = {Performances Shiny : cache, profilage et asynchrone avec
    bindCache et profvis},
  date = {2026-07-16},
  url = {https://www.datanovia.com/learn/programming/shiny/performance/performance-optimization},
  langid = {fr}
}
Veuillez citer ce travail comme suit :
“Performances Shiny : cache, profilage et asynchrone avec bindCache et profvis.” 2026. July 16. https://www.datanovia.com/learn/programming/shiny/performance/performance-optimization.