diff --git a/benchmarks/server_rendering_benchmark.rb b/benchmarks/server_rendering_benchmark.rb new file mode 100644 index 00000000..274d3a12 --- /dev/null +++ b/benchmarks/server_rendering_benchmark.rb @@ -0,0 +1,68 @@ +# - `gem install duktape` +# - Remove `JavaScriptCore` from RUNTIMES if you're not on mac +# - `ruby -I lib benchmarks/server_rendering_benchmark.rb` + +require 'react-rails' +require 'duktape' +require 'benchmark' + +SLOW_COMPONENT = " +var SlowComponent = React.createClass({ + render: function() { + var rand = 0 + for (var i = 0; i < 10000; i++) { + rand = rand + (Math.random() * i) + } + return React.createElement('h1', rand + ' :)') + } +}) +" + +REACT_JS_PATH = File.expand_path("../../vendor/react/react.js", __FILE__) +JS_CODE = File.read(REACT_JS_PATH) + SLOW_COMPONENT + +React::ServerRendering.renderer = React::ServerRendering::ExecJSRenderer +React::ServerRendering.renderer_options = {code: JS_CODE} +React::ServerRendering.pool_timeout = 1000 + +def test_runtime(runtime, renders:, pool_size:, threaded:, thread_size:) + ExecJS.runtime = runtime + React::ServerRendering.pool_size = pool_size + React::ServerRendering.reset_pool + if threaded + threads = thread_size.times.map do + Thread.new do + renders.times do + React::ServerRendering.render("SlowComponent", {}, {}) + Thread.pass + end + end + end + threads.map(&:join) + else + renders.times.map do + React::ServerRendering.render("SlowComponent", {}, {}) + end + end +end + +RUNTIMES = [ + ExecJS::Runtimes::RubyRacer, + ExecJS::Runtimes::Duktape, + ExecJS::Runtimes::JavaScriptCore, + ExecJS::Runtimes::Node, +] + +Benchmark.bm(45) do |x| + [true, false].each do |threaded| + [1, 10, 25].each do |pool_size| + [1, 2, 4, 8].each do |thread_size| + RUNTIMES.each do |runtime| + x.report("#{threaded ? "threaded, " : ""}#{pool_size} conn, #{thread_size} threads, #{runtime.name}") do + test_runtime(runtime, renders: 100, pool_size: pool_size, threaded: true, thread_size: thread_size) + end + end + end + end + end +end diff --git a/lib/react/server_rendering.rb b/lib/react/server_rendering.rb index c379be1a..401e78ef 100644 --- a/lib/react/server_rendering.rb +++ b/lib/react/server_rendering.rb @@ -1,5 +1,6 @@ require 'connection_pool' require 'react/server_rendering/sprockets_renderer' +require 'react/server_rendering/exec_js_renderer' module React module ServerRendering diff --git a/lib/react/server_rendering/exec_js_renderer.rb b/lib/react/server_rendering/exec_js_renderer.rb new file mode 100644 index 00000000..ba851543 --- /dev/null +++ b/lib/react/server_rendering/exec_js_renderer.rb @@ -0,0 +1,31 @@ +module React + module ServerRendering + class ExecJSRenderer + def initialize(options={}) + js_code = options.fetch(:code) || raise("Pass `code:` option to instantiate a JS context!") + @context = ExecJS.compile(js_code) + end + + def render(component_name, props, prerender_options) + js_code = <<-JS + (function () { + var result = React.renderToString(React.createElement(#{component_name}, #{props})); + return result; + })() + JS + + @context.eval(js_code) + rescue ExecJS::ProgramError => err + raise PrerenderError.new(component_name, props, err) + end + + class PrerenderError < RuntimeError + def initialize(component_name, props, js_message) + message = ["Encountered error \"#{js_message}\" when prerendering #{component_name} with #{props}", + js_message.backtrace.join("\n")].join("\n") + super(message) + end + end + end + end +end diff --git a/lib/react/server_rendering/sprockets_renderer.rb b/lib/react/server_rendering/sprockets_renderer.rb index 12928537..100c0e53 100644 --- a/lib/react/server_rendering/sprockets_renderer.rb +++ b/lib/react/server_rendering/sprockets_renderer.rb @@ -8,12 +8,21 @@ def initialize(options={}) js_code = GLOBAL_WRAPPER + CONSOLE_POLYFILL filenames.each do |filename| - js_code << ::Rails.application.assets[filename].to_s + js_code << load_asset(filename) end @context = ExecJS.compile(js_code) end + def load_asset(file) + if ::Rails.application.config.assets.compile + ::Rails.application.assets[file].to_s + else + asset_path = ActionView::Base.new.asset_path(file) + File.read(File.join(::Rails.public_path, asset_path)) + end + end + def render(component_name, props, prerender_options) # pass prerender: :static to use renderToStaticMarkup react_render_method = if prerender_options == :static