mirror of
https://github.com/django-components/django-components.git
synced 2025-08-03 13:58:16 +00:00
Performance (+50%): Compile ComponentNode at creation, not render (#22)
Co-authored-by: rbeard0330 <@dul2k3BKW6m>
This commit is contained in:
parent
d61c0fa469
commit
87f9994c81
4 changed files with 66 additions and 17 deletions
42
benchmarks/component_rendering.py
Normal file
42
benchmarks/component_rendering.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from time import perf_counter
|
||||
|
||||
from django.template import Context, Template
|
||||
|
||||
from django_components import component
|
||||
|
||||
from tests.django_test_setup import * # NOQA
|
||||
from tests.testutils import Django111CompatibleSimpleTestCase as SimpleTestCase
|
||||
|
||||
|
||||
class SlottedComponent(component.Component):
|
||||
def template(self, context):
|
||||
return "slotted_template.html"
|
||||
|
||||
|
||||
class SimpleComponent(component.Component):
|
||||
def context(self, variable, variable2="default"):
|
||||
return {
|
||||
"variable": variable,
|
||||
"variable2": variable2,
|
||||
}
|
||||
|
||||
def template(self, context):
|
||||
return "simple_template.html"
|
||||
|
||||
|
||||
class RenderBenchmarks(SimpleTestCase):
|
||||
def setUp(self):
|
||||
component.registry.clear()
|
||||
component.registry.register('test_component', SlottedComponent)
|
||||
component.registry.register('inner_component', SimpleComponent)
|
||||
|
||||
def test_render_time(self):
|
||||
template = Template("{% load component_tags %}{% component_block 'test_component' %}"
|
||||
"{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}"
|
||||
"{% endcomponent_block %}", name='root')
|
||||
start_time = perf_counter()
|
||||
for _ in range(1000):
|
||||
template.render(Context({}))
|
||||
end_time = perf_counter()
|
||||
total_elapsed = end_time - start_time # NOQA
|
||||
print(f'{total_elapsed } ms per template')
|
|
@ -28,6 +28,7 @@ class Component(with_metaclass(MediaDefiningClass)):
|
|||
|
||||
def __init__(self, component_name):
|
||||
self.__component_name = component_name
|
||||
self.instance_template = None
|
||||
|
||||
def context(self):
|
||||
return {}
|
||||
|
@ -54,14 +55,15 @@ class Component(with_metaclass(MediaDefiningClass)):
|
|||
def slots_in_template(template):
|
||||
return {node.name: node.nodelist for node in template.template.nodelist if is_slot_node(node)}
|
||||
|
||||
def render(self, context, slots_filled=None):
|
||||
slots_filled = slots_filled or {}
|
||||
def compile_instance_template(self, slots_for_instance):
|
||||
"""Use component's base template and the slots used for this instance to compile
|
||||
a unified template for this instance."""
|
||||
|
||||
template = get_template(self.template(context))
|
||||
slots_in_template = self.slots_in_template(template)
|
||||
component_template = get_template(self.template({}))
|
||||
slots_in_template = self.slots_in_template(component_template)
|
||||
|
||||
defined_slot_names = set(slots_in_template.keys())
|
||||
filled_slot_names = set(slots_filled.keys())
|
||||
filled_slot_names = set(slots_for_instance.keys())
|
||||
unexpected_slots = filled_slot_names - defined_slot_names
|
||||
if unexpected_slots:
|
||||
if settings.DEBUG:
|
||||
|
@ -71,20 +73,21 @@ class Component(with_metaclass(MediaDefiningClass)):
|
|||
)
|
||||
)
|
||||
for unexpected_slot in unexpected_slots:
|
||||
del slots_filled[unexpected_slot]
|
||||
del slots_for_instance[unexpected_slot]
|
||||
|
||||
combined_slots = dict(slots_in_template, **slots_filled)
|
||||
combined_slots = dict(slots_in_template, **slots_for_instance)
|
||||
if combined_slots:
|
||||
# Replace slot nodes with their nodelists, then combine into a single, flat nodelist
|
||||
node_iterator = ([node] if not is_slot_node(node) else combined_slots[node.name]
|
||||
for node in template.template.nodelist)
|
||||
for node in component_template.template.nodelist)
|
||||
|
||||
template = copy(template.template)
|
||||
template.nodelist = NodeList(chain.from_iterable(node_iterator))
|
||||
self.instance_template = copy(component_template.template)
|
||||
self.instance_template.nodelist = NodeList(chain.from_iterable(node_iterator))
|
||||
else:
|
||||
template = template.template
|
||||
self.instance_template = component_template.template
|
||||
|
||||
return template.render(context)
|
||||
def render(self, context):
|
||||
return self.instance_template.render(context)
|
||||
|
||||
class Media:
|
||||
css = {}
|
||||
|
|
|
@ -120,15 +120,17 @@ def do_slot(parser, token, component=None):
|
|||
|
||||
class ComponentNode(Node):
|
||||
def __init__(self, component, context_args, context_kwargs, slots=None, isolated_context=False):
|
||||
self.slots = defaultdict(NodeList)
|
||||
for slot in slots or []:
|
||||
self.slots[slot.name].extend(slot.nodelist)
|
||||
self.context_args = context_args or []
|
||||
self.context_kwargs = context_kwargs or {}
|
||||
self.component, self.isolated_context = component, isolated_context
|
||||
slot_dict = defaultdict(NodeList)
|
||||
if slots:
|
||||
for slot in slots:
|
||||
slot_dict[slot.name].extend(slot.nodelist)
|
||||
self.component.compile_instance_template(slot_dict)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Component Node: %s. Contents: %r>" % (self.component, self.slots)
|
||||
return "<Component Node: %s. Contents: %r>" % (self.component, self.component.instance_template.nodelist)
|
||||
|
||||
def render(self, context):
|
||||
self.component.outer_context = context.flatten()
|
||||
|
@ -146,7 +148,7 @@ class ComponentNode(Node):
|
|||
context = context.new()
|
||||
|
||||
with context.update(component_context):
|
||||
return self.component.render(context, slots_filled=self.slots)
|
||||
return self.component.render(context)
|
||||
|
||||
|
||||
@register.tag("component_block")
|
||||
|
|
|
@ -32,6 +32,7 @@ class ComponentRegistryTest(SimpleTestCase):
|
|||
|
||||
comp = SimpleComponent("simple_component")
|
||||
context = Context(comp.context(variable="test"))
|
||||
comp.compile_instance_template({})
|
||||
|
||||
self.assertHTMLEqual(comp.render_dependencies(), dedent("""
|
||||
<link href="style.css" type="text/css" media="all" rel="stylesheet">
|
||||
|
@ -70,6 +71,7 @@ class ComponentRegistryTest(SimpleTestCase):
|
|||
|
||||
comp = FilteredComponent("filtered_component")
|
||||
context = Context(comp.context(var1="test1", var2="test2"))
|
||||
comp.compile_instance_template({})
|
||||
|
||||
self.assertHTMLEqual(comp.render(context), dedent("""
|
||||
Var1: <strong>test1</strong>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue