Skip to content

Browser freezes when streaming (very) long markdown responses #164

@JamesHWade

Description

@JamesHWade

When streaming long markdown content, the browser can freeze as markdown is re-parsed on every incoming chunk. This is especially noticeable when using LLMs with tool calls, where each tool result can add substantial content to the stream.

Reprex
library(shiny)
library(bslib)
library(shinychat)
library(coro)

# Simulate a long streaming response with "tool call" outputs
fake_stream <- async_generator(function(input) {
  yield("I'll run several analyses for you.\n\n")
  await(async_sleep(0.2))

  for (i in 1:8) {
    yield(sprintf("**Running tool %d: analyze_data**\n\n", i))
    await(async_sleep(0.05))

    # Large code block (simulates tool output)
    code <- paste0("```r\n# Results for dataset ", i, "\n",
      paste(sprintf("row_%02d <- c(%s)\n", 1:40,
        sapply(1:40, function(x) paste(sample(1:99, 8), collapse = ", "))),
        collapse = ""), "```\n\n")

    for (chunk in strsplit(code, "(?<=.{50})", perl = TRUE)[[1]]) {
      yield(chunk)
      await(async_sleep(0.008))
    }

    # Markdown table
    table <- paste0(
      "| Metric | Value | SE | CI Low | CI High | p |\n",
      "|--------|-------|-----|--------|---------|------|\n",
      paste(sprintf("| %s | %.2f | %.2f | %.2f | %.2f | %.4f |\n",
        c("Mean","Median","SD","Var","Skew","Kurt","Min","Max","Range","IQR"),
        runif(10,0,100), runif(10,0,5), runif(10,-5,0),
        runif(10,0,5), runif(10,0,0.1)), collapse = ""), "\n")

    for (char in strsplit(table, "")[[1]]) {
      yield(char)
      await(async_sleep(0.003))
    }
  }
  yield("All analyses complete!")
})

ui <- page_fillable(
  p("Send any message to trigger a long streaming response."),
  chat_ui("chat", fill = TRUE)
)

server <- function(input, output, session) {
  observeEvent(input$chat_user_input, {
    chat_append("chat", fake_stream(input$chat_user_input))
  })
}

shinyApp(ui, server)

The browser becomes increasingly unresponsive as the response grows, eventually freezing for several seconds. In extreme cases, it completely freezes that app.

Proposed solution

I have a local fix that implements incremental markdown rendering. It uses block-based rendering and throttling based on content size.

Would a PR be welcome? If so, should I only modify the .ts or should I also build and copy to both packages?

Abbreviated Session info

shiny     1.12.1.9000
bslib     0.9.0.9002
coro      1.1.0
shinychat 0.3.0.9000

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions