Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce PureComponent to reduce `render` method calling #10

Merged
merged 3 commits into from Jul 22, 2019

Conversation

@pocke
Copy link
Contributor

commented Jul 19, 2019

What is PureComponent?

PureComponent is a Component, but it caches render's result to skip render method calling if the attributes are same.
I got the idea from React's PureComponent.

Example

class Hello < Ovto::PureComponent
  def render(name:)
    o 'h1', "Hello, #{compute_name_with_heavy_process(name)}"
  end

  private def compute_name_with_heavy_process(name)
    name.do_some_heavy_process
  end
end


class MainComponent < Ovto::Component
  def render
    o 'div' do
      o Hello, name: state.name
      o 'div', state.something
    end
  end
end

In this example, Hello#render has a heavy function. But it is only called when state.name is changed because it uses PureComponent. It is not called when state.something is changed.
So it will be faster than without PureComponent.

Limitation

PureComponent only supports component that does not use state.
So state method is not available for PureComponent.

PureComponent decides using cache by attributes changes.
If it is aware of state, it will be meaningless. Because state is always changed when render is called.

But I think we can support state with the manual specifying state, which is like shouldUpdateComponent of React.
For example

class C < Ovto::Component
  def render
    o 'h1', state.name
  end

  def should_update?(attr, next_attr, state, next_state)
    # List dependent states manually
    state.name != next_state.name
  end
end

But I do not like this idea. It needs listing all of the dependent states manually. If I forget a dependency, it will cause a mysterious bug.

And we can add the should_update? feature in the future if it is needed.
So I think this feature is not necessary for the first release of PureComponent.

Benchmarking

I profiled my application that is based on Ovto with/without PureComponent.
https://puzzle.pocke.me/

It is a Number Place (a.k.a. Sudoku) application. And I profiled clicking a cell.
When clicking a cell, it calls render method only for the clicked cell if the cell component is a PureComponent.

With PureComponent
190719220922

Without PureComponent
190719221017

As you can see, the second peek is the render method calling. (The first peek is handling the click event.)
The render time is about 29ms (765ms ~ 794ms) without PureComponent, but it is about 5.5ms (692ms ~ 697.5ms).
It reduces 23.5ms, it is 5.3x faster.

I don't think 23.5ms is a meaningful difference. I cannot distinguish the difference.
But I think it will be a meaningful difference if the application will be larger, or the user uses a low-tech computer, such as a cheap smartphone.
If the difference is 4~5x, which is 100ms, I guess I will be annoyed by the delay.

Implementation

This pull request will change the Component (NOT PureComponent) implementation.
It will cache child components instances by this change because component instance should remember the previous attributes.

By the way, the PureComponent implementation is super simple. See the code.


What do you think? If you accept this proposal, I'll do the following TODOs.

  • Write test
  • Write documentation
@yhara

This comment has been minimized.

Copy link
Owner

commented Jul 20, 2019

This looks nice to have 👍 Thank you.

end

def do_render(args, state)
return @cache if args == @prev_props

This comment has been minimized.

Copy link
@yhara

yhara Jul 20, 2019

Owner

How about using #equal? instead of #== ?

This comment has been minimized.

Copy link
@pocke

pocke Jul 21, 2019

Author Contributor

#equal? does not work, because component receives different hash instance every time.

def render
  o Pure, {foo: bar} # `{foo: bar}` hash is created by every time of `render` calling.
end

By the way, I guess we can use equal? for all value of args. e.g.) args.all?{|k, v| v.equal? @prev_props[k]}.

This comment has been minimized.

Copy link
@pocke

pocke Jul 21, 2019

Author Contributor

I tried benchmarking, but the naive implementation to use equal? is slower than ==.

note: The example in the previous comment has a bug because it does not check key equivalent.

require 'benchmark'

# Use Struct because there are enough seped difference between #equal? and #==.
obj = Struct.new(:foo, :bar).new(1, 2)

a = { foo: 'bar', hoge: obj }
b = a.dup
c = { foo: 'bar', hoge: obj, additional: 'aaa' }

def use_eqeq(x, y)
 x == y
end

def use_equal(x, y)
 x.keys == y.keys && x.all?{|k, v| y[k] == v}
end


# Just test for use_equal method
p use_eqeq(a, b) # => true
p use_equal(a, b) # => true
p use_eqeq(a, c) # => false
p use_equal(a, c) # => false

N = 100000

Benchmark.bm(20) do |x|
 x.report('Struct#==')     {N.times{obj == obj}}
 x.report('Struct#equal?') {N.times{obj.equal?(obj)}}

 x.report('same ==')    {N.times{use_eqeq(a, b)}}
 x.report('same equal?'){N.times{use_equal(a, b)}}

 x.report('diff ==')    {N.times{use_eqeq(a, c)}}
 x.report('diff equal?'){N.times{use_equal(a, c)}}
end
$ opal test.rb
true
true
false
false
                           user     system      total        real
Struct#==              0.218000   0.218000   0.872000 (  0.217753)
Struct#equal?          0.016000   0.016000   0.064000 (  0.015879)
same ==                0.253000   0.253000   1.012000 (  0.253861)
same equal?            0.498000   0.498000   1.992001 (  0.498554)
diff ==                0.017000   0.017000   0.068000 (  0.017099)
diff equal?            0.085000   0.085000   0.340000 (  0.084590)

I guess we can improve speed by implementing it with JavaScript, like Hash#==.
But I think it is over-engineering, so we can use Hash#==. It is enough fast.

pocke added some commits Jul 21, 2019

@pocke

This comment has been minimized.

Copy link
Contributor Author

commented Jul 22, 2019

I wrote the test and documentation.

Thank you!

@yhara yhara merged commit d1c4c29 into yhara:master Jul 22, 2019

@pocke pocke deleted the pocke:pure-component branch Jul 22, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.