├── .gitignore ├── README.md ├── css ├── interface.css └── interface.less ├── gulpfile.js ├── index.html ├── package.json ├── resources ├── benchmark.js ├── bluebird.min.js ├── highcharts.js ├── loader.imba ├── loader.js ├── lodash.js └── shared │ ├── api.js │ └── app.css ├── results.png ├── results@2x.png ├── server.js └── todomvc ├── backbone ├── .gitignore ├── index.html ├── js │ ├── api.js │ ├── app.js │ ├── collections │ │ └── todos.js │ ├── models │ │ └── todo.js │ ├── routers │ │ └── router.js │ └── views │ │ ├── app-view.js │ │ └── todo-view.js ├── package.json └── readme.md ├── imba-0.14.3 ├── imba │ ├── app.imba │ ├── todoItem.imba │ └── todoModel.imba ├── index.html ├── js │ ├── api.js │ ├── app.js │ ├── imba.min.js │ ├── todoItem.js │ └── todoModel.js ├── package.json └── readme.md ├── imba-dev ├── app.js ├── index.html ├── package.json ├── readme.md └── src │ ├── app.imba │ ├── store.imba │ └── todo.imba ├── mithril ├── .gitignore ├── ext │ └── mithril │ │ ├── mithril.js │ │ └── mithril.min.js ├── index.html ├── js │ ├── api.js │ ├── app.js │ ├── controllers │ │ └── todo.js │ ├── models │ │ ├── storage.js │ │ └── todo.js │ └── views │ │ ├── footer-view.js │ │ └── main-view.js ├── package.json └── readme.md └── react ├── changelog.md ├── external ├── director.js ├── react-with-addons.js └── react-with-addons.min.js ├── index.html ├── js ├── api.js ├── app.js ├── footer.js ├── todoItem.js ├── todoModel.js └── utils.js ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Benchmarking raw rendering performance of TodoMVC implementations. 2 | 3 | The benchmark can be run [here](http://somebee.github.io/todomvc-render-benchmark/index.html) 4 | 5 | ## Why 6 | There has been a TodoMVC benchmark floating around earlier. It mainly tested the performance of your browser, by creating fake events and navigating the dom to insert, complete, and remove todos. Some frameworks such as elm and mithril outperformed React by a lot. The (only) reason for this was that they used asynchronous rendering, while React rendered / synchronized the whole view every time a todo was inserted, completed, and removed. Making rendering in React asynchronous would take 2 lines of code, and bring its performance up to the others. A better benchmark is needed. 7 | 8 | ## Goal 9 | The goal of this benchmark is to test the performance of pure rendering / [dom reconciliation](https://facebook.github.io/react/docs/reconciliation.html). It inserts, renames, toggles, moves, and removes todos through an API that must be custom made for each TodoMVC implementation. These actions should happen as low-level as possible, and should *not* trigger any sort of rendering. API.render() however should trigger a full forced rendering / reconciliation. 10 | 11 | ## Results 12 | On OSX 10.10.4, Chrome: 13 | 14 | ![Results](https://raw.githubusercontent.com/somebee/todomvc-render-benchmark/master/results%402x.png) 15 | 16 | As you can see, Imba is *much* faster than the other implementations. So much faster in fact, that your first reaction is likely along the line "this seems fishy". Well, it really is that fast. It uses a very different approach from existing virtual dom implementations, by inlining and reusing actual dom nodes on subsequent renders. This is only practical to do in a compiled language (such as [Imba](http://imba.io)) - as the compiler can analyze the views, and do inline caching that would be very chaotic and cumbersome in plain js. 17 | 18 | Imba enables a new way of writing web apps. Fully syncing the view is now so inexpensive that it is, for the first time, viable to simply render on every single frame, without any need for listeners, bindings, dependency tracking etc. Keeping the view 'in sync' is no longer an issue. It is like rendering templates on the server - where you always know the whole state/story on render. If you for some reason want to keep tracking and only rerendering subviews etc this is as easy as it ever was with React. 19 | 20 | Even though it looks incredibly boring, the "Unchanged Render" is possibly the most interesting benchmark. All of these frameworks are plenty fast if they only render whenever something has changed. But if the frameworks are fast enough to render the whole app in 0.01ms, you could skip thinking about all sorts of tracking to keep the view in sync. 21 | 22 | ## Spec 23 | A spec for the API is underway, but until then you can look at the default [api.js](https://github.com/somebee/todomvc-render-benchmark/blob/master/resources/api.js). 24 | 25 | ## Contribute 26 | I am sure that the different implementations can be tweaked for better performance, and I'm happy to apply such improvements if you send a pull request. The improvements should however, adhere to the philosophy of the framework and maintain readability. 27 | 28 | ## PS 29 | Neither Imba nor React are using their respective shouldComponentUpdate in this benchmark, as this would not be fair to Mithril. Again, this is intended to really compare the speeds of a full synchronous rendering of the whole app. The relative performance difference between Imba and React is not affected by this. When optimizations are active, both are slightly faster. 30 | 31 | Some framework are not made for this at all, and it does not make sense to include them in this benchmark. This is meant to test the performance of bringing the whole view in sync with the models, like if you have no bindings or anything. Angular is included in the repo, but disabled for now.It does tons of stuff under the hood, and it is difficult for me to know what would be like a _full_ rerendering in Angular. It might not even be relevant. 32 | -------------------------------------------------------------------------------- /css/interface.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | div { 7 | box-sizing: border-box; 8 | } 9 | body { 10 | font-family: 'Lucida Grande', 'Trebuchet MS', 'Bitstream Vera Sans', Verdana, Helvetica, sans-serif; 11 | background-color: #fdfdfd; 12 | } 13 | ol { 14 | list-style: none; 15 | margin: 5px 0; 16 | padding: 0; 17 | } 18 | ol ol { 19 | margin-left: 2em; 20 | list-position: outside; 21 | } 22 | nav { 23 | position: absolute; 24 | right: 10px; 25 | } 26 | .button { 27 | -webkit-appearance: none; 28 | padding: 8px 20px; 29 | background-color: #4d596f; 30 | color: white; 31 | font-weight: bold; 32 | font-size: inherit; 33 | text-decoration: none; 34 | font-size: 12px; 35 | margin-right: 8px; 36 | border: none; 37 | border-radius: 2px; 38 | } 39 | .button:hover { 40 | background-color: #3B4A67; 41 | } 42 | .button[disabled] { 43 | opacity: 0.5; 44 | background-color: #3B4A67; 45 | } 46 | #top { 47 | position: relative; 48 | height: 64px; 49 | display: none; 50 | } 51 | button { 52 | -webkit-appearance: none; 53 | padding: 8px 20px; 54 | background-color: #4d596f; 55 | color: white; 56 | font-weight: bold; 57 | font-size: inherit; 58 | text-decoration: none; 59 | font-size: 12px; 60 | margin-right: 8px; 61 | border: none; 62 | border-radius: 2px; 63 | } 64 | button:hover { 65 | background-color: #3B4A67; 66 | } 67 | button[disabled] { 68 | opacity: 0.5; 69 | background-color: #3B4A67; 70 | } 71 | #controls { 72 | width: 100%; 73 | min-height: 64px; 74 | padding: 10px; 75 | vertical-align: top; 76 | background: #202837; 77 | color: white; 78 | position: relative; 79 | top: 0px; 80 | left: 0px; 81 | z-index: 1000; 82 | display: -webkit-flex; 83 | display: -ms-flexbox; 84 | display: flex; 85 | -webkit-flex-direction: row; 86 | -ms-flex-direction: row; 87 | flex-direction: row; 88 | -webkit-align-items: center; 89 | -ms-flex-align: center; 90 | align-items: center; 91 | -webkit-flex-wrap: wrap; 92 | -ms-flex-wrap: wrap; 93 | flex-wrap: wrap; 94 | } 95 | #controls button { 96 | margin: 10px; 97 | } 98 | footer { 99 | padding: 10px 20px; 100 | } 101 | #analysis { 102 | padding: 10px 40px; 103 | } 104 | #analysis:empty { 105 | display: none; 106 | } 107 | #analysis .chart { 108 | height: 240px; 109 | } 110 | #apps { 111 | padding: 10px 10px; 112 | position: relative; 113 | } 114 | #apps > div { 115 | width: 50%; 116 | padding: 10px; 117 | position: relative; 118 | display: inline-block; 119 | } 120 | .running button { 121 | pointer-events: none; 122 | opacity: 0.5; 123 | } 124 | #apps > div > .header { 125 | width: 100%; 126 | position: relative; 127 | height: 30px; 128 | color: #202837; 129 | background: #e8e8e8; 130 | line-height: 30px; 131 | padding: 0px 10px; 132 | border-top-left-radius: 3px; 133 | border-top-right-radius: 3px; 134 | font-size: 12px; 135 | font-weight: bold; 136 | } 137 | iframe { 138 | border: 4px solid #e8e8e8; 139 | border-top: none; 140 | margin: 0; 141 | padding: 0; 142 | vertical-align: top; 143 | width: 100%; 144 | box-sizing: border-box; 145 | height: 550px; 146 | } 147 | #apps > div.running iframe { 148 | border-color: #202837; 149 | } 150 | #apps > div.running .header { 151 | background-color: #202837; 152 | color: white; 153 | } 154 | @media (max-width: 1000px) { 155 | #apps > div { 156 | width: 50%; 157 | } 158 | } 159 | @media (max-width: 600px) { 160 | #apps > div { 161 | width: 100%; 162 | display: block; 163 | } 164 | } 165 | @media (max-width: 500px) { 166 | #analysis { 167 | padding: 10px 10px; 168 | } 169 | #analysis .chart { 170 | height: 340px; 171 | } 172 | #controls { 173 | position: relative; 174 | height: auto; 175 | } 176 | #apps > div { 177 | width: 100%; 178 | } 179 | #apps > div iframe { 180 | display: none; 181 | } 182 | #apps > div.running iframe { 183 | display: block; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /css/interface.less: -------------------------------------------------------------------------------- 1 | html,body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | div { 7 | box-sizing: border-box; 8 | } 9 | 10 | body { 11 | font-family: 'Lucida Grande','Trebuchet MS','Bitstream Vera Sans',Verdana,Helvetica,sans-serif; 12 | background-color: rgb(253, 253, 253); 13 | } 14 | ol { list-style: none; margin: 5px 0; padding: 0; } 15 | ol ol { margin-left: 2em; list-position: outside; } 16 | nav { position: absolute; right: 10px; } 17 | 18 | .button { 19 | -webkit-appearance: none; 20 | padding: 8px 20px; 21 | background-color: rgb(77, 89, 111); 22 | color: white; 23 | font-weight: bold; 24 | font-size: inherit; 25 | text-decoration: none; 26 | font-size: 12px; 27 | margin-right: 8px; 28 | border: none; 29 | border-radius: 2px; 30 | 31 | &:hover { 32 | background-color: #3B4A67; 33 | } 34 | 35 | &[disabled] { 36 | opacity: 0.5; 37 | background-color: #3B4A67; 38 | } 39 | } 40 | 41 | #top { 42 | position: relative; 43 | height: 64px; 44 | display: none; 45 | } 46 | 47 | button { 48 | .button; 49 | } 50 | 51 | #controls { 52 | width: 100%; 53 | min-height: 64px; 54 | padding: 10px; 55 | vertical-align: top; 56 | background: #202837; 57 | color: white; 58 | position: relative; 59 | top: 0px; 60 | left: 0px; 61 | z-index: 1000; 62 | display: flex; 63 | flex-direction: row; 64 | align-items: center; 65 | flex-wrap: wrap; 66 | 67 | button { 68 | margin: 10px; 69 | } 70 | } 71 | 72 | footer { 73 | padding: 10px 20px; 74 | } 75 | 76 | #analysis { 77 | padding: 10px 40px; 78 | 79 | &:empty { display: none; } 80 | } 81 | 82 | #analysis .chart { 83 | height: 240px; 84 | } 85 | 86 | #apps { 87 | padding: 10px 10px; 88 | position: relative; 89 | } 90 | 91 | #apps > div { 92 | 93 | width:50%; 94 | padding: 10px; 95 | position: relative; 96 | display: inline-block; 97 | } 98 | 99 | 100 | 101 | .running button { 102 | pointer-events: none; 103 | opacity: 0.5; 104 | } 105 | 106 | #apps > div > .header { 107 | width: 100%; 108 | position: relative; 109 | height: 30px; 110 | color: #202837; 111 | background: rgb(232, 232, 232); 112 | line-height: 30px; 113 | padding: 0px 10px; 114 | border-top-left-radius: 3px; 115 | border-top-right-radius: 3px; 116 | font-size: 12px; 117 | font-weight: bold; 118 | } 119 | 120 | iframe { 121 | border: 4px solid rgb(232, 232, 232); 122 | border-top: none; 123 | margin: 0; 124 | padding: 0; 125 | vertical-align: top; 126 | width: 100%; 127 | box-sizing: border-box; 128 | height: 550px; 129 | } 130 | 131 | #apps > div.running iframe { 132 | border-color: #202837; 133 | } 134 | 135 | #apps > div.running .header { 136 | background-color: #202837; 137 | color: white; 138 | } 139 | 140 | 141 | 142 | @media (max-width: 1000px) { 143 | #apps > div { width: 50%; } 144 | } 145 | 146 | @media (max-width: 600px) { 147 | #apps > div { width: 100%; display: block; } 148 | } 149 | 150 | @media (max-width: 500px) { 151 | #analysis { padding: 10px 10px; } 152 | #analysis .chart { 153 | height: 340px; 154 | } 155 | 156 | #controls { position: relative; height: auto; } 157 | #apps > div { width: 100%; } 158 | #apps > div iframe { display: none; } 159 | #apps > div.running iframe { display: block; } 160 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var less = require('gulp-less'); 3 | 4 | var LessPluginAutoPrefix = require('less-plugin-autoprefix'), 5 | autoprefix = new LessPluginAutoPrefix({ browsers: ["last 2 versions"] }); 6 | 7 | 8 | gulp.task('less',function(){ 9 | gulp.src('./css/**/*.less') 10 | .pipe(less({plugins: [autoprefix]})) 11 | .pipe(gulp.dest('./css')); 12 | }) 13 | 14 | gulp.task('watch',function(){ 15 | gulp.watch('./css/**/*.less', ['less']); 16 | }) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TodoMVC Render Benchmark 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-render-perf", 3 | "description": "Benchmarking for TodoMVC", 4 | "keywords": [ 5 | "imba", 6 | "react", 7 | "angular", 8 | "todomvc" 9 | ], 10 | "author": "Sindre Aarsaether", 11 | "bugs": "https://github.com/somebee/todomvc-render-perf/issues", 12 | "version": "0.1.0", 13 | "licenses": [ 14 | { 15 | "type": "MIT", 16 | "url": "http://github.com/somebee/todomvc-render-perf/raw/master/LICENSE" 17 | } 18 | ], 19 | "engines": { 20 | "node": ">=0.8.0" 21 | }, 22 | "files": [], 23 | "directories": {}, 24 | "main": "./index.js", 25 | "homepage": "http://somebee.github.io/todomvc-render-perf/index.html", 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/somebee/todomvc-render-perf.git" 29 | }, 30 | "devDependencies": { 31 | "marked": "^0.3.3" 32 | }, 33 | "dependencies": { 34 | "express": "^4.13.2", 35 | "gulp": "^3.9.0", 36 | "gulp-less": "^3.0.3", 37 | "less": "^2.5.1", 38 | "less-plugin-autoprefix": "^1.4.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/loader.imba: -------------------------------------------------------------------------------- 1 | 2 | Manager = 3 | _apps: [] 4 | _suites: [] 5 | 6 | def Manager.add suite 7 | @suites.push(suite) 8 | 9 | def Manager.suites 10 | @suites 11 | 12 | def Manager.chart series 13 | if @chart 14 | @chart.addSeries(series) 15 | return @chart 16 | 17 | var categories = @suites.map(|suite| suite.option('label') ) 18 | 19 | @chart = Highcharts.Chart.new({ 20 | chart: { type: 'bar', renderTo: 'chart' } 21 | title: { text: "Results" } 22 | loading: {showDuration: 50 } 23 | xAxis: 24 | categories: [] # categories 25 | 26 | yAxis: 27 | min: 0 28 | title: { text: 'ops / sec (higher is better)'} 29 | 30 | tooltip: 31 | pointFormatter: do |v| "{this:category} " + this:y.toFixed(2) + " ops/sec
" 32 | shared: true 33 | 34 | plotOptions: {bar: {dataLabels: { enabled: false}}} 35 | credits: { enabled: false } 36 | series: [series] 37 | }) 38 | 39 | def div cls, text 40 | var el = document.createElement('div') 41 | el:className = cls or '' 42 | el:textContent = text or '' 43 | return el 44 | 45 | class Framework 46 | 47 | var dict = {} 48 | var all = [] 49 | 50 | def self.get name 51 | dict[name] 52 | 53 | def self.map fn 54 | all.map(fn) 55 | 56 | def self.count 57 | all:length 58 | 59 | def self.build 60 | @build ||= Promise.reduce(all) do |curr,next| 61 | curr.build.then do 62 | Promise.delay(100).then do next.build 63 | 64 | def name 65 | @name 66 | 67 | def title 68 | @title 69 | 70 | def initialize name, o = {} 71 | dict[name] = self 72 | all.push(self) 73 | @name = name 74 | @title = o:title or name 75 | @options = o 76 | @ready = false 77 | 78 | def color 79 | @options:color or 'red' 80 | 81 | def url 82 | @options:url || "todomvc/{@name}/index.html" 83 | 84 | def node 85 | @node ||= div() 86 | 87 | def iframe 88 | @iframe ||= document.createElement('iframe') 89 | 90 | def doc 91 | @iframe:contentDocument 92 | 93 | def win 94 | @win ||= @iframe:contentWindow 95 | 96 | def api 97 | @api ||= @iframe:contentWindow.API 98 | 99 | def build 100 | @build ||= Promise.new do |resolve| 101 | iframe:style:minHeight = '400px' 102 | iframe:src = url 103 | iframe:id = "{@name}_frame" 104 | window:apps.appendChild(node) 105 | node.appendChild(@header = div('header',title)) 106 | node.appendChild(iframe) 107 | 108 | var wait = do 109 | if doc.querySelector('#header h1,.header h1') && api.RENDERCOUNT > 0 110 | api.ready 111 | prepare 112 | return resolve(self) 113 | setTimeout(&,10) do wait() 114 | wait() 115 | 116 | def prepare 117 | # win:localStorage.clear 118 | reset(6) 119 | 120 | def reset count 121 | api.AUTORENDER = no 122 | # api.clearAllTodos 123 | api.addTodo(("Todo " + i)) for i in [1..count] 124 | @todoCount = count 125 | api.render(true) 126 | api.AUTORENDER = yes 127 | self 128 | 129 | def deactivate 130 | node:classList.remove('running') 131 | 132 | def activate 133 | node:classList.add('running') 134 | 135 | def status= status 136 | @header:textContent = status 137 | self 138 | 139 | 140 | class Bench 141 | 142 | def option key 143 | @options[key] 144 | 145 | def initialize o = {} 146 | @name = o:title 147 | @suite = Benchmark.Suite.new @name 148 | @options = o 149 | @step = -1 150 | @current = null 151 | @benchmarks = [] 152 | 153 | if o:step isa Function 154 | Framework.map do |app| 155 | @suite.add(app.name, o:step) 156 | var bm = @suite[@suite:length - 1] 157 | bm.App = app 158 | @benchmarks.push(bm) 159 | 160 | console.log @suite 161 | Manager.add self 162 | bind 163 | self 164 | 165 | def step idx 166 | @step = idx 167 | if @current 168 | @current.App.deactivate 169 | 170 | if @current = @benchmarks[idx] 171 | @current.App.activate 172 | self 173 | 174 | 175 | def bind 176 | 177 | @suite.on 'start' do |e| 178 | console.log "start" 179 | document:body:classList.add('running') 180 | 181 | Framework.map do |ex| 182 | ex.api.FULLRENDER = yes 183 | ex.api.RENDERCOUNT = -1 184 | ex.api.render(yes) 185 | step(0) 186 | return 187 | 188 | @suite.on 'reset' do |e| 189 | console.log 'suite onReset' 190 | return 191 | 192 | @suite.on 'cycle' do |event| 193 | console.log "cycle!" 194 | Framework.get(event:target:name).status = String(event:target) 195 | step(@step + 1) 196 | return 197 | 198 | @suite.on 'complete' do 199 | console.log('Fastest is ' + this.filter('fastest').pluck('name')) 200 | document:body:classList.remove('running') 201 | present 202 | return 203 | 204 | def run 205 | Framework.build.then do 206 | # @benchmarks.map do |b| b:hz = Math.random * 40000 207 | # present 208 | @suite.run { async: true, queued: false } 209 | 210 | def reset 211 | Framework.map do |ex| 212 | ex.api.FULLRENDER = yes 213 | ex.api.RENDERCOUNT = -1 214 | ex.api.render(yes) 215 | self 216 | 217 | def warmup times = 1000 218 | reset 219 | setTimeout(&,50) do 220 | 221 | var fn = @options:step 222 | var apps = Framework.map do |app| app 223 | var step = do 224 | if var app = apps.shift 225 | var i = 0 226 | var bm = {App: app} 227 | var start = Date.new 228 | while i++ < times 229 | fn.call(bm) 230 | var elapsed = Date.new - start 231 | app.status = "{app.title} - {@name} - {times} iterations - {elapsed}ms" 232 | setTimeout(&,50) do step() 233 | 234 | step() 235 | 236 | return self 237 | 238 | 239 | def present 240 | # create div 241 | console.log 'present' 242 | var el = div('chart') 243 | window:analysis.appendChild(el) 244 | 245 | # find slowest 246 | var sorted = @benchmarks.slice.sort do |a,b| a:hz - b:hz 247 | var base = sorted[0][:hz] 248 | var series = @benchmarks.map do |b| {type: 'bar', borderWidth: 0, name: b.App.title, data: [b:hz] } 249 | 250 | @chart = Highcharts.Chart.new({ 251 | chart: { type: 'bar', renderTo: el } 252 | 253 | title: 254 | text: option('title') 255 | style: 256 | fontSize: "14px" 257 | 258 | loading: 259 | showDuration: 0 260 | 261 | xAxis: 262 | categories: [option('title')] 263 | tickColor: 'transparent' 264 | labels: { enabled: false } 265 | 266 | yAxis: 267 | min: 0 268 | title: { text: 'ops / sec (higher is better)'} 269 | 270 | tooltip: 271 | pointFormatter: do |v| "" + this:y.toFixed(2) + " ops/sec ({(this:y / base).toFixed(2)}x)
" 272 | # shared: true 273 | 274 | legend: 275 | verticalAlign: 'top' 276 | y: 20 277 | 278 | plotOptions: {bar: {dataLabels: { enabled: true, formatter: (|v| "{(this:y / base).toFixed(2)}x") }}} 279 | credits: { enabled: false } 280 | series: series.reverse 281 | }) 282 | 283 | 284 | Framework.new('react', title: 'react v0.13.3') 285 | Framework.new('imba-0.14.3', title: 'imba v0.14.3') 286 | Framework.new('imba-dev', title: 'imba v0.15.0-alpha.1') 287 | Framework.new('mithril', title: 'mithril v0.2.0') 288 | 289 | EVERYTHING = Bench.new 290 | label: 'Bench Everything' 291 | title: 'Everything (remove, toggle, append, rename)' 292 | step: do 293 | var len = this.App.@todoCount 294 | var api = this.App.api 295 | var idx = Math.round(Math.random * (len - 1)) 296 | 297 | # moving a random task 298 | var idx = api.RENDERCOUNT % len 299 | var idx = Math.min(0,len - 2) 300 | var todo = api.removeTodoAtIndex(idx) 301 | api.insertTodoAtIndex(todo,1000) 302 | 303 | api.render(yes) 304 | api.toggleTodoAtIndex((idx) % len) 305 | api.render(yes) 306 | api.renameTodoAtIndex((idx + 1) % len,"Todo - {api.RENDERCOUNT}") 307 | api.render(yes) 308 | return 309 | 310 | Bench.new 311 | label: 'Reorder' 312 | title: 'Reorder (shift+push)' 313 | step: do 314 | var api = this.App.api 315 | var todo = api.removeTodoAtIndex(0) 316 | api.render(true) 317 | api.insertTodoAtIndex(todo,1000) # at the end 318 | api.render(true) 319 | return 320 | 321 | # full rendering including todo-renaming 322 | Bench.new 323 | label: 'Rename todo' 324 | title: 'Rename random todo' 325 | step: do 326 | var api = this.App.api 327 | var i = 0 328 | var c = api.RENDERCOUNT 329 | var idx = Math.round(Math.random * (this.App.@todoCount - 1)) 330 | api.renameTodoAtIndex(idx,"Todo {idx + 1} {c}",no) 331 | api.render(true) # render at the very end 332 | return 333 | 334 | Bench.new 335 | label: 'Toggle todo' 336 | title: 'Toggle random todo' 337 | step: do 338 | var api = this.App.api 339 | var idx = Math.round(Math.random * (this.App.@todoCount - 1)) 340 | api.toggleTodoAtIndex(idx) 341 | api.render(true) 342 | return 343 | 344 | 345 | # full rendering 346 | Bench.new 347 | label: 'Unchanged render' 348 | title: 'Unchanged render' 349 | step: do this.App.api.render(yes) 350 | 351 | 352 | Manager.suites.map do |suite| 353 | var btn = document.createElement('button') 354 | btn:textContent = suite.option('label') 355 | btn:onclick = do 356 | btn:disabled = "disabled" 357 | suite.run 358 | window:controls.appendChild(btn) 359 | 360 | 361 | # window:runFullRender:onclick = do full.run 362 | # Suites.fullRender.run({ async: true, queued: false }) 363 | 364 | window:apps.setAttribute("data-count",Framework.count) 365 | 366 | Framework.build.then do |res| 367 | console.log "built",res 368 | Promise.delay(200).then do 369 | document.getElementsByTagName('button')[0].focus 370 | -------------------------------------------------------------------------------- /resources/loader.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | Manager = { 4 | _apps: [], 5 | _suites: [] 6 | }; 7 | 8 | Manager.add = function (suite){ 9 | return this._suites.push(suite); 10 | }; 11 | 12 | Manager.suites = function (){ 13 | return this._suites; 14 | }; 15 | 16 | Manager.chart = function (series){ 17 | if (this._chart) { 18 | this._chart.addSeries(series); 19 | return this._chart; 20 | }; 21 | 22 | var categories = this._suites.map(function(suite) { return suite.option('label'); }); 23 | 24 | return this._chart = new Highcharts.Chart({ 25 | chart: {type: 'bar',renderTo: 'chart'}, 26 | title: {text: "Results"}, 27 | loading: {showDuration: 50}, 28 | xAxis: { 29 | categories: [] // categories 30 | }, 31 | 32 | yAxis: { 33 | min: 0, 34 | title: {text: 'ops / sec (higher is better)'} 35 | }, 36 | 37 | tooltip: { 38 | pointFormatter: function(v) { return ("" + (this.category) + " ") + this.y.toFixed(2) + " ops/sec
"; }, 39 | shared: true 40 | }, 41 | 42 | plotOptions: {bar: {dataLabels: {enabled: false}}}, 43 | credits: {enabled: false}, 44 | series: [series] 45 | }); 46 | }; 47 | 48 | function div(cls,text){ 49 | var el = document.createElement('div'); 50 | el.className = cls || ''; 51 | el.textContent = text || ''; 52 | return el; 53 | }; 54 | 55 | function Framework(name,o){ 56 | if(o === undefined) o = {}; 57 | dict[name] = this; 58 | all.push(this); 59 | this._name = name; 60 | this._title = o.title || name; 61 | this._options = o; 62 | this._ready = false; 63 | }; 64 | 65 | var dict = {}; 66 | var all = []; 67 | 68 | Framework.get = function (name){ 69 | return dict[name]; 70 | }; 71 | 72 | Framework.map = function (fn){ 73 | return all.map(fn); 74 | }; 75 | 76 | Framework.count = function (){ 77 | return all.length; 78 | }; 79 | 80 | Framework.build = function (){ 81 | return this._build || (this._build = Promise.reduce(all,function(curr,next) { 82 | return curr.build().then(function() { 83 | return Promise.delay(100).then(function() { return next.build(); }); 84 | }); 85 | })); 86 | }; 87 | 88 | Framework.prototype.name = function (){ 89 | return this._name; 90 | }; 91 | 92 | Framework.prototype.title = function (){ 93 | return this._title; 94 | }; 95 | 96 | Framework.prototype.color = function (){ 97 | return this._options.color || 'red'; 98 | }; 99 | 100 | Framework.prototype.url = function (){ 101 | return this._options.url || ("todomvc/" + (this._name) + "/index.html"); 102 | }; 103 | 104 | Framework.prototype.node = function (){ 105 | return this._node || (this._node = div()); 106 | }; 107 | 108 | Framework.prototype.iframe = function (){ 109 | return this._iframe || (this._iframe = document.createElement('iframe')); 110 | }; 111 | 112 | Framework.prototype.doc = function (){ 113 | return this._iframe.contentDocument; 114 | }; 115 | 116 | Framework.prototype.win = function (){ 117 | return this._win || (this._win = this._iframe.contentWindow); 118 | }; 119 | 120 | Framework.prototype.api = function (){ 121 | return this._api || (this._api = this._iframe.contentWindow.API); 122 | }; 123 | 124 | Framework.prototype.build = function (){ 125 | var self = this; 126 | return self._build || (self._build = new Promise(function(resolve) { 127 | self.iframe().style.minHeight = '400px'; 128 | self.iframe().src = self.url(); 129 | self.iframe().id = ("" + (self._name) + "_frame"); 130 | window.apps.appendChild(self.node()); 131 | self.node().appendChild(self._header = div('header',self.title())); 132 | self.node().appendChild(self.iframe()); 133 | 134 | var wait = function() { 135 | if (self.doc().querySelector('#header h1,.header h1') && self.api().RENDERCOUNT > 0) { 136 | self.api().ready(); 137 | self.prepare(); 138 | return resolve(self); 139 | }; 140 | return setTimeout(function() { return wait(); },10); 141 | }; 142 | return wait(); 143 | })); 144 | }; 145 | 146 | Framework.prototype.prepare = function (){ 147 | // win:localStorage.clear 148 | return this.reset(6); 149 | }; 150 | 151 | Framework.prototype.reset = function (count){ 152 | this.api().AUTORENDER = false; 153 | // api.clearAllTodos 154 | for (var len = count, i = 1; i <= len; i++) { 155 | this.api().addTodo(("Todo " + i)); 156 | }; 157 | this._todoCount = count; 158 | this.api().render(true); 159 | this.api().AUTORENDER = true; 160 | return this; 161 | }; 162 | 163 | Framework.prototype.deactivate = function (){ 164 | return this.node().classList.remove('running'); 165 | }; 166 | 167 | Framework.prototype.activate = function (){ 168 | return this.node().classList.add('running'); 169 | }; 170 | 171 | Framework.prototype.setStatus = function (status){ 172 | this._header.textContent = status; 173 | this; 174 | return this; 175 | }; 176 | 177 | 178 | function Bench(o){ 179 | var self = this; 180 | if(o === undefined) o = {}; 181 | this._name = o.title; 182 | this._suite = new Benchmark.Suite(this._name); 183 | this._options = o; 184 | this._step = -1; 185 | this._current = null; 186 | this._benchmarks = []; 187 | 188 | if (o.step instanceof Function) { 189 | Framework.map(function(app) { 190 | self._suite.add(app.name(),o.step); 191 | var bm = self._suite[self._suite.length - 1]; 192 | bm.App = app; 193 | return self._benchmarks.push(bm); 194 | }); 195 | }; 196 | 197 | console.log(self._suite); 198 | Manager.add(self); 199 | self.bind(); 200 | self; 201 | }; 202 | 203 | Bench.prototype.option = function (key){ 204 | return this._options[key]; 205 | }; 206 | 207 | Bench.prototype.step = function (idx){ 208 | this._step = idx; 209 | if (this._current) { 210 | this._current.App.deactivate(); 211 | }; 212 | 213 | if (this._current = this._benchmarks[idx]) { 214 | this._current.App.activate(); 215 | }; 216 | return this; 217 | }; 218 | 219 | 220 | Bench.prototype.bind = function (){ 221 | 222 | var self = this; 223 | self._suite.on('start',function(e) { 224 | console.log("start"); 225 | document.body.classList.add('running'); 226 | 227 | Framework.map(function(ex) { 228 | ex.api().FULLRENDER = true; 229 | ex.api().RENDERCOUNT = -1; 230 | return ex.api().render(true); 231 | }); 232 | self.step(0); 233 | return; 234 | }); 235 | 236 | self._suite.on('reset',function(e) { 237 | console.log('suite onReset'); 238 | return; 239 | }); 240 | 241 | self._suite.on('cycle',function(event) { 242 | console.log("cycle!"); 243 | Framework.get(event.target.name).setStatus(String(event.target)); 244 | self.step(self._step + 1); 245 | return; 246 | }); 247 | 248 | return self._suite.on('complete',function() { 249 | console.log('Fastest is ' + this.filter('fastest').pluck('name')); 250 | document.body.classList.remove('running'); 251 | self.present(); 252 | return; 253 | }); 254 | }; 255 | 256 | Bench.prototype.run = function (){ 257 | var self = this; 258 | return Framework.build().then(function() { 259 | // @benchmarks.map do |b| b:hz = Math.random * 40000 260 | // present 261 | return self._suite.run({async: true,queued: false}); 262 | }); 263 | }; 264 | 265 | Bench.prototype.reset = function (){ 266 | Framework.map(function(ex) { 267 | ex.api().FULLRENDER = true; 268 | ex.api().RENDERCOUNT = -1; 269 | return ex.api().render(true); 270 | }); 271 | return this; 272 | }; 273 | 274 | Bench.prototype.warmup = function (times){ 275 | var self = this; 276 | if(times === undefined) times = 1000; 277 | this.reset(); 278 | setTimeout(function() { 279 | 280 | var fn = self._options.step; 281 | var apps = Framework.map(function(app) { return app; }); 282 | var step = function() { 283 | var app; 284 | if (app = apps.shift()) { 285 | var i = 0; 286 | var bm = {App: app}; 287 | var start = new Date(); 288 | while (i++ < times){ 289 | fn.call(bm); 290 | }; 291 | var elapsed = new Date() - start; 292 | app.setStatus(("" + (app.title()) + " - " + (self._name) + " - " + times + " iterations - " + elapsed + "ms")); 293 | return setTimeout(function() { return step(); },50); 294 | }; 295 | }; 296 | 297 | return step(); 298 | },50); 299 | 300 | return self; 301 | }; 302 | 303 | 304 | Bench.prototype.present = function (){ 305 | // create div 306 | console.log('present'); 307 | var el = div('chart'); 308 | window.analysis.appendChild(el); 309 | 310 | // find slowest 311 | var sorted = this._benchmarks.slice().sort(function(a,b) { return a.hz - b.hz; }); 312 | var base = sorted[0].hz; 313 | var series = this._benchmarks.map(function(b) { return {type: 'bar',borderWidth: 0,name: b.App.title(),data: [b.hz]}; }); 314 | 315 | return this._chart = new Highcharts.Chart({ 316 | chart: {type: 'bar',renderTo: el}, 317 | 318 | title: { 319 | text: this.option('title'), 320 | style: { 321 | fontSize: "14px" 322 | } 323 | }, 324 | 325 | loading: { 326 | showDuration: 0 327 | }, 328 | 329 | xAxis: { 330 | categories: [this.option('title')], 331 | tickColor: 'transparent', 332 | labels: {enabled: false} 333 | }, 334 | 335 | yAxis: { 336 | min: 0, 337 | title: {text: 'ops / sec (higher is better)'} 338 | }, 339 | 340 | tooltip: { 341 | pointFormatter: function(v) { return "" + this.y.toFixed(2) + (" ops/sec (" + (this.y / base).toFixed(2) + "x)
"); } 342 | // shared: true 343 | }, 344 | 345 | legend: { 346 | verticalAlign: 'top', 347 | y: 20 348 | }, 349 | 350 | plotOptions: {bar: {dataLabels: {enabled: true,formatter: function(v) { return ("" + (this.y / base).toFixed(2) + "x"); }}}}, 351 | credits: {enabled: false}, 352 | series: series.reverse() 353 | }); 354 | }; 355 | 356 | 357 | new Framework('react',{title: 'react v0.13.3'}); 358 | new Framework('imba-0.14.3',{title: 'imba v0.14.3'}); 359 | new Framework('imba-dev',{title: 'imba v0.15.0-alpha.1'}); 360 | new Framework('mithril',{title: 'mithril v0.2.0'}); 361 | 362 | EVERYTHING = new Bench( 363 | {label: 'Bench Everything', 364 | title: 'Everything (remove, toggle, append, rename)', 365 | step: function() { 366 | var len = this.App._todoCount; 367 | var api = this.App.api(); 368 | var idx = Math.round(Math.random() * (len - 1)); 369 | 370 | // moving a random task 371 | idx = api.RENDERCOUNT % len; 372 | idx = Math.min(0,len - 2); 373 | var todo = api.removeTodoAtIndex(idx); 374 | api.insertTodoAtIndex(todo,1000); 375 | 376 | api.render(true); 377 | api.toggleTodoAtIndex((idx) % len); 378 | api.render(true); 379 | api.renameTodoAtIndex((idx + 1) % len,("Todo - " + (api.RENDERCOUNT))); 380 | api.render(true); 381 | return; 382 | }} 383 | ); 384 | 385 | new Bench( 386 | {label: 'Reorder', 387 | title: 'Reorder (shift+push)', 388 | step: function() { 389 | var api = this.App.api(); 390 | var todo = api.removeTodoAtIndex(0); 391 | api.render(true); 392 | api.insertTodoAtIndex(todo,1000); // at the end 393 | api.render(true); 394 | return; 395 | }} 396 | ); 397 | 398 | // full rendering including todo-renaming 399 | new Bench( 400 | {label: 'Rename todo', 401 | title: 'Rename random todo', 402 | step: function() { 403 | var api = this.App.api(); 404 | var i = 0; 405 | var c = api.RENDERCOUNT; 406 | var idx = Math.round(Math.random() * (this.App._todoCount - 1)); 407 | api.renameTodoAtIndex(idx,("Todo " + (idx + 1) + " " + c),false); 408 | api.render(true); // render at the very end 409 | return; 410 | }} 411 | ); 412 | 413 | new Bench( 414 | {label: 'Toggle todo', 415 | title: 'Toggle random todo', 416 | step: function() { 417 | var api = this.App.api(); 418 | var idx = Math.round(Math.random() * (this.App._todoCount - 1)); 419 | api.toggleTodoAtIndex(idx); 420 | api.render(true); 421 | return; 422 | }} 423 | ); 424 | 425 | 426 | // full rendering 427 | new Bench( 428 | {label: 'Unchanged render', 429 | title: 'Unchanged render', 430 | step: function() { return this.App.api().render(true); }} 431 | ); 432 | 433 | 434 | Manager.suites().map(function(suite) { 435 | var btn = document.createElement('button'); 436 | btn.textContent = suite.option('label'); 437 | btn.onclick = function() { 438 | btn.disabled = "disabled"; 439 | return suite.run(); 440 | }; 441 | return window.controls.appendChild(btn); 442 | }); 443 | 444 | 445 | // window:runFullRender:onclick = do full.run 446 | // Suites.fullRender.run({ async: true, queued: false }) 447 | 448 | window.apps.setAttribute("data-count",Framework.count()); 449 | 450 | return Framework.build().then(function(res) { 451 | console.log("built",res); 452 | return Promise.delay(200).then(function() { 453 | return document.getElementsByTagName('button')[0].focus(); 454 | }); 455 | }); 456 | 457 | })(); -------------------------------------------------------------------------------- /resources/shared/api.js: -------------------------------------------------------------------------------- 1 | 2 | API = { 3 | // Always on for now 4 | AUTORENDER: true, 5 | 6 | AUTOPERSIST: false, 7 | 8 | // Force full render / reconcile for apps 9 | // that include manual shortcuts like 10 | // React.shouldComponentUpdate (same in Imba) 11 | FULLRENDER: true, 12 | 13 | // This counter should always increase by one whenever 14 | // the implmenentation rerenders, and be displayed in 15 | // the actual view as part of the title (

) 16 | //

todos

in regular TodoMVC becomes: 17 | //

todos {{API.RENDERCOUNT}}

or similar 18 | RENDERCOUNT: 0 19 | } 20 | 21 | // clear the whole localStorage on load 22 | localStorage.clear(); 23 | 24 | API.ready = function(){ 25 | // called after TodoMVC has been loaded 26 | // Could be useful for overriding stuff 27 | } 28 | 29 | // synchronous render 30 | // should bring the view in sync with models++ 31 | // no matter how or where the models have changed 32 | API.render = function(force){ 33 | // render app 34 | return API.RENDERCOUNT; 35 | } 36 | 37 | // Add todo with title 38 | // app should NOT render or persist to localstorage 39 | API.addTodo = function(title) { 40 | app.model.addTodo(title); 41 | } 42 | 43 | // expose interface for renaming todo 44 | API.renameTodoAtIndex = function(index,title) { 45 | var todo = app.model.todos[index]; 46 | todo.title = title; 47 | return todo; 48 | } 49 | 50 | API.getTodoAtIndex = function (index){ 51 | return app.model.todos[index]; 52 | }; 53 | 54 | API.insertTodoAtIndex = function (todo,index){ 55 | var list = app.model.todos; 56 | var len = list.length; 57 | var from = list.indexOf(todo); 58 | 59 | if (index >= len) { 60 | list.push(todo); 61 | } else { 62 | list.splice(index,0,todo); 63 | }; 64 | 65 | return todo; 66 | }; 67 | 68 | API.removeTodoAtIndex = function (index){ 69 | var todo = API.getTodoAtIndex(index); 70 | app.model.todos.splice(index,1); 71 | return todo; 72 | }; 73 | 74 | API.clearAllTodos = function() { 75 | app.model.clearAll(); 76 | } 77 | 78 | // return plain array of actual todo items 79 | API.getTodos = function(){ 80 | return app.model.todos; 81 | } 82 | 83 | API.toggleTodoAtIndex = function(index) { 84 | var todo = API.getTodos()[index]; 85 | todo.completed = !todo.completed; 86 | } -------------------------------------------------------------------------------- /resources/shared/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | 380 | 381 | .todoapp h1 { 382 | font-size: 16px; 383 | font-weight: bold; 384 | color: red; 385 | position: absolute; 386 | top: -2em; 387 | line-height: 2em; 388 | margin: 0; 389 | padding: 0; 390 | 391 | transform: translate3d(0px,0px,0px); 392 | -moz-transform: translate3d(0px,0px,0px); 393 | -webkit-transform: translate3d(0px,0px,0px); 394 | 395 | backface-visibility: hidden; 396 | -moz-backface-visibility: hidden; 397 | -webkit-backface-visibility: hidden; 398 | } 399 | 400 | .todoapp { 401 | margin: 40px 0px; 402 | } 403 | 404 | label { 405 | transform: translate3d(0px,0px,0px); 406 | -moz-transform: translate3d(0px,0px,0px); 407 | -webkit-transform: translate3d(0px,0px,0px); 408 | 409 | backface-visibility: hidden; 410 | -moz-backface-visibility: hidden; 411 | -webkit-backface-visibility: hidden; 412 | 413 | } -------------------------------------------------------------------------------- /results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somebee/todomvc-render-benchmark/4fde09cb48756cfa24c4e75cfc6622d7b58117f1/results.png -------------------------------------------------------------------------------- /results@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somebee/todomvc-render-benchmark/4fde09cb48756cfa24c4e75cfc6622d7b58117f1/results@2x.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var express = require('express'); 5 | var fs = require('fs'); 6 | var app = module.exports = express(); 7 | app.use(express.static(__dirname)); 8 | 9 | app.listen(8080); -------------------------------------------------------------------------------- /todomvc/backbone/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/backbone/* 2 | !node_modules/backbone/backbone.js 3 | 4 | node_modules/backbone.localstorage/* 5 | !node_modules/backbone.localstorage/backbone.localStorage.js 6 | 7 | node_modules/jquery/* 8 | !node_modules/jquery/dist 9 | node_modules/jquery/dist/* 10 | !node_modules/jquery/dist/jquery.js 11 | 12 | node_modules/todomvc-app-css/* 13 | !node_modules/todomvc-app-css/index.css 14 | 15 | node_modules/todomvc-common/* 16 | !node_modules/todomvc-common/base.css 17 | !node_modules/todomvc-common/base.js 18 | 19 | node_modules/underscore/* 20 | !node_modules/underscore/underscore.js 21 | -------------------------------------------------------------------------------- /todomvc/backbone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Backbone.js • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 33 |
34 | 38 |
39 | 40 | 41 | 42 |
43 | 44 |
45 | 50 | 58 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /todomvc/backbone/js/api.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/somebee/todomvc-render-benchmark/4fde09cb48756cfa24c4e75cfc6622d7b58117f1/todomvc/backbone/js/api.js -------------------------------------------------------------------------------- /todomvc/backbone/js/app.js: -------------------------------------------------------------------------------- 1 | /*global $ */ 2 | /*jshint unused:false */ 3 | var app = app || {}; 4 | var ENTER_KEY = 13; 5 | var ESC_KEY = 27; 6 | 7 | $(function () { 8 | 'use strict'; 9 | 10 | // Drop localStorage for benchmarks 11 | Backbone.sync = function () {}; 12 | 13 | // kick things off by creating the `App` 14 | API.AppView = new app.AppView(); 15 | API.AppView.render(); 16 | API.isReady = true; 17 | }); 18 | -------------------------------------------------------------------------------- /todomvc/backbone/js/collections/todos.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | // Todo Collection 8 | // --------------- 9 | 10 | // The collection of todos is backed by *localStorage* instead of a remote 11 | // server. 12 | var Todos = Backbone.Collection.extend({ 13 | // Reference to this collection's model. 14 | model: app.Todo, 15 | 16 | // Save all of the todo items under the `"todos"` namespace. 17 | localStorage: new Backbone.LocalStorage('todos-backbone'), 18 | 19 | // Filter down the list of all todo items that are finished. 20 | completed: function () { 21 | return this.where({completed: true}); 22 | }, 23 | 24 | // Filter down the list to only todo items that are still not finished. 25 | remaining: function () { 26 | return this.where({completed: false}); 27 | }, 28 | 29 | // We keep the Todos in sequential order, despite being saved by unordered 30 | // GUID in the database. This generates the next order number for new items. 31 | nextOrder: function () { 32 | return this.length ? this.last().get('order') + 1 : 1; 33 | }, 34 | 35 | // Todos are sorted by their original insertion order. 36 | comparator: 'order' 37 | }); 38 | 39 | // Create our global collection of **Todos**. 40 | app.todos = new Todos(); 41 | })(); 42 | -------------------------------------------------------------------------------- /todomvc/backbone/js/models/todo.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | // Todo Model 8 | // ---------- 9 | 10 | // Our basic **Todo** model has `title`, `order`, and `completed` attributes. 11 | app.Todo = Backbone.Model.extend({ 12 | // Default attributes for the todo 13 | // and ensure that each todo created has `title` and `completed` keys. 14 | defaults: { 15 | title: '', 16 | completed: false 17 | }, 18 | 19 | // Toggle the `completed` state of this todo item. 20 | toggle: function () { 21 | this.save({ 22 | completed: !this.get('completed') 23 | }); 24 | } 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /todomvc/backbone/js/routers/router.js: -------------------------------------------------------------------------------- 1 | /*global Backbone */ 2 | var app = app || {}; 3 | 4 | (function () { 5 | 'use strict'; 6 | 7 | // Todo Router 8 | // ---------- 9 | var TodoRouter = Backbone.Router.extend({ 10 | routes: { 11 | '*filter': 'setFilter' 12 | }, 13 | 14 | setFilter: function (param) { 15 | // Set the current filter to be used 16 | app.TodoFilter = param || ''; 17 | 18 | // Trigger a collection filter event, causing hiding/unhiding 19 | // of Todo view items 20 | app.todos.trigger('filter'); 21 | } 22 | }); 23 | 24 | app.TodoRouter = new TodoRouter(); 25 | Backbone.history.start(); 26 | })(); 27 | -------------------------------------------------------------------------------- /todomvc/backbone/js/views/app-view.js: -------------------------------------------------------------------------------- 1 | /*global Backbone, jQuery, _, ENTER_KEY */ 2 | var app = app || {}; 3 | 4 | (function ($) { 5 | 'use strict'; 6 | 7 | // The Application 8 | // --------------- 9 | 10 | // Our overall **AppView** is the top-level piece of UI. 11 | app.AppView = Backbone.View.extend({ 12 | 13 | // Instead of generating a new element, bind to the existing skeleton of 14 | // the App already present in the HTML. 15 | el: '#todoapp', 16 | 17 | // Our template for the line of statistics at the bottom of the app. 18 | statsTemplate: _.template($('#stats-template').html()), 19 | 20 | // Delegated events for creating new items, and clearing completed ones. 21 | events: { 22 | 'keypress #new-todo': 'createOnEnter', 23 | 'click #clear-completed': 'clearCompleted', 24 | 'click #toggle-all': 'toggleAllComplete' 25 | }, 26 | 27 | // At initialization we bind to the relevant events on the `Todos` 28 | // collection, when items are added or changed. Kick things off by 29 | // loading any preexisting todos that might be saved in *localStorage*. 30 | initialize: function () { 31 | this.allCheckbox = this.$('#toggle-all')[0]; 32 | this.$input = this.$('#new-todo'); 33 | this.$h1 = this.$('#header h1')[0]; 34 | this.$footer = this.$('#footer'); 35 | this.$main = this.$('#main'); 36 | this.$list = $('#todo-list'); 37 | 38 | this.listenTo(app.todos, 'add', this.addOne); 39 | this.listenTo(app.todos, 'reset', this.addAll); 40 | this.listenTo(app.todos, 'change:completed', this.filterOne); 41 | this.listenTo(app.todos, 'filter', this.filterAll); 42 | this.listenTo(app.todos, 'all', _.debounce(this.render, 0)); 43 | 44 | // Suppresses 'add' events with {reset: true} and prevents the app view 45 | // from being re-rendered for every model. Only renders when the 'reset' 46 | // event is triggered at the end of the fetch. 47 | app.todos.fetch({reset: true}); 48 | }, 49 | 50 | // Re-rendering the App just means refreshing the statistics -- the rest 51 | // of the app doesn't change. 52 | render: function () { 53 | API.RENDERCOUNT++; 54 | 55 | var completed = app.todos.completed().length; 56 | var remaining = app.todos.remaining().length; 57 | 58 | this.$h1.textContent = "todos "+API.RENDERCOUNT; 59 | 60 | if (app.todos.length) { 61 | this.$main.show(); 62 | this.$footer.show(); 63 | 64 | this.$footer.html(this.statsTemplate({ 65 | completed: completed, 66 | remaining: remaining 67 | })); 68 | 69 | this.$('#filters li a') 70 | .removeClass('selected') 71 | .filter('[href="#/' + (app.TodoFilter || '') + '"]') 72 | .addClass('selected'); 73 | } else { 74 | this.$main.hide(); 75 | this.$footer.hide(); 76 | } 77 | 78 | if(API.FULLRENDER){ 79 | app.todos.each(function(item){ item.$view.render(); }); 80 | } 81 | 82 | this.allCheckbox.checked = !remaining; 83 | }, 84 | 85 | // Add a single todo item to the list by creating a view for it, and 86 | // appending its element to the `