rstudio/shiny — pull request #179 by wch (2013-06-18 05:08:58+00:00)
Title: Faster tags

As reported in https://groups.google.com/d/msg/shiny-discuss/doHpFM6ZOGg/aKdut4HaHvkJ

Creating a `selectInput` with thousands of elements was very slow. This patch speeds up `selectInput`.

```
library(shiny)
n_choices <- 2000
choices <- paste0("choice", 1:n_choices)
names(choices) <- paste(choices, "name")
system.time({
s <- selectInput("select", "Select:", multiple = TRUE, choices = choices)
})

### Old version:
user system elapsed
2.017 0.017 2.035

### New version (in this pull request):
user system elapsed
0.112 0.002 0.114
```

The slowdown happened because a list was being modified in-place in a loop.

I also tried to speed up `radioButtons`, but the improvement was small. The problem here has to do with putting a list of tags directly in a call to a `tag$foo` function, vs. passing that list to `tagAppendChild`. The latter is actually much faster. For example:

```
n_choices <- 5000
choices <- paste0("choice", 1:n_choices)
p_tags <- lapply(choices, function(x) tags$p(x))

system.time({
z1 <- tags$div(class="foo", p_tags)
})
# user system elapsed
# 10.021 0.025 10.065

system.time({
z2 <- tags$div(class="foo")
z2 <- tagAppendChild(z2, p_tags)
})
# user system elapsed
# 0.004 0.000 0.004
```

`z1` and `z2` result in the same HTML output, but the structure isn't identical: `z2` has one more level of nested list. Here are the objects' structures (with only 2 items inside instead of 5000):

```
> str(z1)
List of 3
$ name : chr "div"
$ attribs :List of 1
..$ class: chr "foo"
$ children:List of 2
..$ :List of 3
.. ..$ name : chr "p"
.. ..$ attribs : list()
.. ..$ children:List of 1
.. .. ..$ : chr "choice1"
.. ..- attr(*, "class")= chr "shiny.tag"
..$ :List of 3
.. ..$ name : chr "p"
.. ..$ attribs : list()
.. ..$ children:List of 1
.. .. ..$ : chr "choice2"
.. ..- attr(*, "class")= chr "shiny.tag"
- attr(*, "class")= chr "shiny.tag"

> str(z2)
List of 3
$ name : chr "div"
$ attribs :List of 1
..$ class: chr "foo"
$ children:List of 1
..$ :List of 2
.. ..$ :List of 3
.. .. ..$ name : chr "p"
.. .. ..$ attribs : list()
.. .. ..$ children:List of 1
.. .. .. ..$ : chr "choice1"
.. .. ..- attr(*, "class")= chr "shiny.tag"
.. ..$ :List of 3
.. .. ..$ name : chr "p"
.. .. ..$ attribs : list()
.. .. ..$ children:List of 1
.. .. .. ..$ : chr "choice2"
.. .. ..- attr(*, "class")= chr "shiny.tag"
- attr(*, "class")= chr "shiny.tag"
```

I believe the same is true for my modified version of `selectInput`: there's one more nesting level in the tag data structure, but the resulting HTML is the same. Ideally, there wouldn't be this extra layer.

Some things to consider:
* Speed up the `tags$foo()` functions so they don't iteratively modify lists in-place.
* Allow `tagsAppendChild()` to take a list so that a list of items can be added directly as children. (Maybe make `tagsAppendChildren()` externally visible?)
* Related to previous item, update my version of `selectInput` so that it doesn't add an extra level of nesting.