From 87f9994c81078a02be9d6013a65c5710b29bb674 Mon Sep 17 00:00:00 2001 From: rbeard0330 Date: Sat, 6 Feb 2021 04:09:57 -0500 Subject: [PATCH] Performance (+50%): Compile ComponentNode at creation, not render (#22) Co-authored-by: rbeard0330 <@dul2k3BKW6m> --- benchmarks/component_rendering.py | 42 +++++++++++++++++++ django_components/component.py | 27 ++++++------ .../templatetags/component_tags.py | 12 +++--- tests/test_component.py | 2 + 4 files changed, 66 insertions(+), 17 deletions(-) create mode 100644 benchmarks/component_rendering.py diff --git a/benchmarks/component_rendering.py b/benchmarks/component_rendering.py new file mode 100644 index 00000000..5afba81e --- /dev/null +++ b/benchmarks/component_rendering.py @@ -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') diff --git a/django_components/component.py b/django_components/component.py index 294167b0..2dfea5a7 100644 --- a/django_components/component.py +++ b/django_components/component.py @@ -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 = {} diff --git a/django_components/templatetags/component_tags.py b/django_components/templatetags/component_tags.py index 452545b2..8016b591 100644 --- a/django_components/templatetags/component_tags.py +++ b/django_components/templatetags/component_tags.py @@ -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 "" % (self.component, self.slots) + return "" % (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") diff --git a/tests/test_component.py b/tests/test_component.py index 1f02f6e1..dcffc35f 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -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(""" @@ -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: test1