The Hidden Cost of Eager Loading in PHP
60sReveals a common performance pitfall and sets up a problem developers can relate to, increasing curiosity.
▶ Play Clip"Delivers a solid deep dive into lazy objects for DI, though it could be more concise."
This video explains how PHP 8.4's Lazy Objects feature revolutionizes dependency injection by allowing services to be loaded only when actually needed, significantly reducing overhead from autoloading and object construction. The presenter demonstrates the concept with a simple container example and shows how Symfony's Dependency Injection Container implements this feature.
Dependency injection eagerly loads all objects into memory, creating a large object graph. Many dependencies are never used during a request, wasting time on autoloading and construction.
A newsletter controller has recursive dependencies: newsletter manager, entity manager, mailer, SMTP mailer. All are constructed even if not needed.
With lazy loading, objects are not instantiated until used. Configure newsletter manager and entity manager as lazy objects, saving construction time for unused sub-dependencies.
Symfony used proxy manager libraries (ProxyManager, ProxyManager LTS) and Symfony VarExporter to generate proxy code, because PHP lacked native support.
The RFC, contributed by Arnaud and Nicolas, adds two methods to ReflectionClass: newLazyGhost and newLazyProxy. Ghost initializes on property access; proxy delegates to real object.
Create a lazy ghost: ReflectionClass::newLazyGhost(initializer). Access triggers initializer. Lazy proxy requires initializer to return the real object.
A simple DI container with get and register methods. Services created once and returned. Profiling shows entity manager creation takes ~5ms, wasted if unused.
Modify container to always create lazy proxies. Use ReflectionClass::newLazyProxy with initializer calling create method. Transparent to code.
Lazy container runs in 7ms vs ~12ms before. Entity manager constructor and driver connection not called. Only necessary objects are loaded.
Symfony supports lazy services via YAML/XML/PHP attributes. Services are not lazy by default, but some (like event listeners) are. Doctrine repositories and entity manager are often lazy.
Symfony generates PHP code for the container to avoid parsing config on each request. Factory methods use newLazyGhost with initializer that calls constructor when needed.
PHP 8.4's Lazy Objects provide a native, efficient way to implement lazy loading in DI containers, reducing overhead by only constructing objects that are actually used. This feature is a significant improvement for performance-sensitive PHP applications.
What is the main problem with eager loading in dependency injection?
It eagerly loads all objects into memory, creating a large object graph, even if many dependencies are never used during a request, wasting time on autoloading and construction.
00:15
What are the two new methods added to ReflectionClass in PHP 8.4?
newLazyGhost and newLazyProxy.
04:13
How does a lazy ghost object differ from a lazy proxy?
A ghost object looks like the real object and initializes itself on property access. A lazy proxy acts as a delegate, loading the real object and delegating calls to it.
04:30
What was the performance improvement observed in the demo when using lazy loading?
The script ran in 7ms instead of ~12ms, and the entity manager constructor and driver connection were not called.
12:38
How does Symfony decide whether to use a lazy ghost or lazy proxy?
Symfony has clever code to decide based on the service type, but the details are not covered in the video.
21:05
PHP 8.4 Lazy Objects RFC
This RFC introduces native lazy loading to PHP, eliminating the need for external proxy libraries and code generation.
03:33Future of DI Containers
The presenter predicts that future DI containers will make services lazy by default due to the significant performance benefits.
10:43Profiling Results
Demonstrates a concrete performance improvement (from ~12ms to 7ms) by avoiding unnecessary object construction.
12:38Symfony's Investment
Symfony invested in lazy loading before PHP 8.4, showing the importance of this optimization in real-world frameworks.
16:28[00:03] have a massive effect on dependency injection in PHP. And in this video, I will explain how with a simple container example and the Symfony DIC.
[00:15] What is this feature solving for dependency injection? The problem with dependency injection is that it eagerly loads all objects into memory uh by creating them, autoloading them and constructing them into a big object
[00:29] graph. But when many of these dependencies are never used during a request, the cost of autoloading and construction of these objects and their memory allocation wears the performance down. Before composer solved autoloading
[00:43] once and for all, the cost of autoloading in autoload was a commonly discussed topic. As in this blog post by Matthew from 2010, but the autoloading overhead does did not just magically disappear with composer. It still
[00:59] occupies a major share of PHP request time. Morning. I'm Benjamin and me and my team have focused only on PHP performance for the last 10 years and companies, and open-source projects along the way. Let's look at an example
[01:14] of a newsletter controller and its recursive dependency tree. All objects need to be autoloaded, created, and their constructor being called. And then they are nested into each other. In this case, we have the newsletter controller
[01:28] on top um and the newsletter manager and entity manager as direct dependencies. And they each have different other dependencies like the newsletter manager uses a mailer object and the entity manager from doctrine requires the event
[01:44] manager configuration, the proxy factory and essentially many more objects that uh are not shown here. With lady loading, we can cut the object graph and
[01:56] the objects are not instantiated before they are used. In this example here, we are configuring both the newsletter manager and the entity manager to be manager and the entity manager to be lazy objects. Meaning that they are only
[02:09] constructed virtually to look like entity managers and newsletter managers, constructed. So their constructor is not being called and all the dependencies
[02:21] for the constructor are also not passed to them. This saves the autoating and construction time for the subobjects or sort of the children of children and if they're not needed during the request we
[02:35] save the time entirely. In Symfony, this feature is implemented using the um lazy flag on the service which can be um lazy flag on the service which can be configured in attributes or XML. Before
[02:50] configured in attributes or XML. Before PHP 8.4, Symfony used the proxy manager by Marco and its fork, the proxy manager LTS uh to have wider support for PHP versions. And lately um they introduced
[03:04] the Symfony bar exporter component. Um and all of these used code generation to implement proxies because before PHP 8.4 it was not possible to create
[03:17] um objects looking like entity manager and newsletter manager and um they are not being completely initialized. So this was done through code generation before with PHP 8.4 for the lazy objects RFC
[03:33] and these libraries are not necess not needed anymore at all and the PHP engine provides this proxy functionality in in its core for much greater flexibility
[03:45] lower performance overhead and most importantly it requires no code generation anymore. Let's quickly review the lazy objects RFC. It was contributed the lazy objects RFC. It was contributed by Ano and Nicolas for version 8.4. The
[04:00] discussion was controversial and detailed, but in the end it was passed detailed, but in the end it was passed and added to PHP 8.4. and added to PHP 8.4. The RFC adds two primary new methods to
[04:13] the refraction class uh object. New lazy ghost and new lazy proxy. With them, you ghost and new lazy proxy. With them, you can create a ghost and proxy objects. A ghost object looks like the real object and then initializes itself to be the
[04:30] real object once uh any property is accessed accessed and the lazy proxy works uh as sort of a and the lazy proxy works uh as sort of a delegate. So the proxy is created and
[04:43] delegate. So the proxy is created and once it's accessed then um the the the original object will be loaded and sort of be used as a delegate being called uh instead of the the proxy object. So they work slightly different and um the
[05:02] both implementations are ne necessary for different use cases but um to understand and look at this it's essentially not necessary to look at essentially not necessary to look at them in detail. How do we use them? So
[05:15] it's quite easy. We have a class my class and uh we specify an initializer class and uh we specify an initializer here. And if we create a ghost object for it, we have a reflection class of this one. Then create a ghost with the
[05:29] initializer. And then at this point we have a lazy ghost object. And once we have a lazy ghost object. And once we access the lazy ghost um the initializer access the lazy ghost um the initializer closure will be called which um sort of
[05:42] lazily calls the constructor. And um the same is true for a lazy proxy. The lazy proxy as I said before works a little bit different. um from the initializer of the lazy proxy, you need to create the new class or return
[05:56] the new class um that is then used um uh as a proxy uh object being delegated to. Let's look at the newsletter controller example from before in code to try to
[06:09] understand how proxy objects work or lazy proxies work in PHP 8.4. I have written a PHP dependency injection container here that's very simple. It has two methods. Get uh where I pass a class name that I want to
[06:26] instantiate as a service and a second method register where I register um sort of the factory method for that service. And the services are only created once and then returned over and over again. and the factory method um receives the
[06:43] container to be able to uh recursively get services to create um the the object structure. So let's look at using this uh in our example. We register the newsletter controller
[06:58] and we get the container here. The newsletter controller has newsletter manager and entity manager as dependencies. And then we have the newsletter manager receiving the mailer. The mailer
[07:11] receiving the mailer. The mailer receiving the SMTP mailer and the SMTP mailer has no dependencies in our example. And then here we create the example. And then here we create the entity manager using um doctrines or M
[07:25] entity manager as a real example. Using the container then boils down to getting the newsletter controller calling send newsletter on it for
[07:37] calling send newsletter on it for example. And in our case um let's open um
[07:51] is actually echoing a message from the SMTP mailer. SMTP mailer. So here we see send and the SMTP mailer is recursively called from send. The newsletter manager calls it and the
[08:07] newsletter controller calls it. So this code here uh requires the newsletter manager. It's using the newsletter manager and all its dependencies. The entity manager is injected but it's not not being used at all.
[08:21] So if we look at that from a profiler we can see the overhead of creating this object graph without um some of these object being uh needed. We uh run
[08:33] tideways uh to gather a call graph and we look at the call graph in our browser
[08:46] our browser and we can see on the right in the call and we can see on the right in the call graph that there is some overhead calling driver manager get connection and creating the conject uh connection.
[08:59] and creating the conject uh connection. If we go through the core graph, we can see this is uh related to the closure in container PHP line 50 to 55. Let's uh
[09:11] look at that code. And um we see um we see this is the code here creating the this is the code here creating the entity manager.
[09:32] essentially takes yeah five milliseconds for creating the the driver connection everything. So it's quite expensive to create this entity manager object in this case on the CLI. So in a
[09:48] um in a non CLI context this will be much faster. But this only shows uh in much faster. But this only shows uh in much more detail like why constructing dependencies that are not needed is a wasteful um uh thing that uh should be
[10:02] optimized. So going back to the code, let's modify it to run um the service creation lazily. Um I've prepared this lazy container here and it looks slightly different
[10:16] than the container before. So we still have the get method here to create a have the get method here to create a service but instead of um instantiating the service by calling the factory method we
[10:31] always create a lazy version of the service. service. So my example container here may will So my example container here may will make every service lazy by default. And
[10:43] this is something I think the future of uh dependency injection containers and uh dependency injection containers and PHP uh will also do because the benefits are just so great by doing everything lazily by default and um switching
[10:57] lazily by default and um switching things to eager um by configuration. Um so we have the refraction class called new lazy proxy here and then the
[11:10] initializer calls the create method um for the service and this is the method that the previous container used and it was the get method there. So here we have the actual creation of the services only
[11:24] when the lazy proxy is initialized and nothing else changed. So this is uh quite a simple uh transition to uh make the container completely lazy and it's
[11:36] uh transparent to the code. Nothing else has to change. So the code still um has to change. So the code still um calls get here and um runs the send newsletter method. So let's switch to a terminal
[11:58] and run the lazy container PHP file with 8.4. Again sending email via SMTP is outputed. So the code runs while the newsletter controller manager mailer and SMTP mailer are used to generate the result and the entity
[12:13] generate the result and the entity manager is not used. And to verify how this looks, we now run this again in the profiler to see what methods are being profiler to see what methods are being called.
[12:38] the the script itself runs in 7 milliseconds. So it's much faster than milliseconds. So it's much faster than before. And the call graph if we look at before. And the call graph if we look at this does not show as a major bottleneck
[12:50] anymore the creation of the doctrine connection and entity manager. So let's search for driver manager get connection. So method does not exist.
[13:02] connection. So method does not exist. It's not being called. So entity manager construct or entity manager entity manager in general also not being called manager in general also not being called at all. So what happens here is the lazy
[13:17] proxy feature from PHP prevents the entity manager from actually being created. We only see sort of a virtual proxy dummy object in the code at proxy dummy object in the code at runtime uh which is not being used.
[13:32] So what about the newsletter controller? So the newsletter controller is So the newsletter controller is constructed. Let's see the uh factory method here. Construct. It's called from the parent at lazy container line 50
[13:48] container create. So we uh we see here that newsletter controller is being constructed. And then if we recursively look into that we
[14:00] then if we recursively look into that we see that the parent method for this is newsletter controller send newsletter actually. So that is quite a weird way of a call graph. So we see newsletter controller an actual instance send
[14:16] newsletter is being called but then under the hood because it's the first under the hood because it's the first time this is being accessed um it initializes the newsletter controller at that point calling the constructor
[14:29] because um sent newsletter needs to access um an instance variable and that instance variables require the the proxy to be
[14:42] loaded. So from a core graph perspective, it looks like super weird how this connects together. Um, but this is totally transparent to the PHP application and it doesn't really realize this at all.
[15:03] newsletter manager service. The entity manager is created as a lazy object and since it's not needed, it's not initialized itself and we save all the initialized itself and we save all the time for creating it. So we can see here
[15:19] time for creating it. So we can see here let's say for the SMTP mailer send method and look at how that one works. works. We see from this newsletter manager send
[15:32] newsletter is being called. Mailer sent is being called. SMTP mailer sent is is being called. SMTP mailer sent is being called. But at every point here we see in the mailer send method it lazily loads from the container or its
[15:45] dependencies. And for the send newsletter as well it lazily loads the dependencies calling the closure in the in the container before it calls the actual method. So we have sort of an
[16:01] um a domino effect of loading lazily these objects um because the code at these objects um because the code at runtime realizes they are necessary. So for applications with very big object graphs this will save a lot of time
[16:16] graphs this will save a lot of time loading them um up front by just really loading only those that are necessary. And um Symfony for example already
[16:28] invested a lot of work into this kind of lazy loading uh before PHP 8.4 got out lazy loading uh before PHP 8.4 got out because they saw in their profiling that it takes a huge amount of time to eagerly create the complete object craft
[16:44] eagerly create the complete object craft and it's better to not do that. So let's look at how Symfony uses the lazy object RFC. I mentioned before that lazy services were a feature of uh Symfony dependency
[16:58] injection even before PHP 8.4 uh came out and looking at the documentation we see the reasoning in some cases you may inject a server that's a bit heavy to instantiate. So the thing that I
[17:12] mentioned before uh expensive construction of an object um either through recursive autoloading and loading the files but also maybe the
[17:24] service is doing actual expensive work in the constructor like um connecting to a service or doing sync like that. uh which is an even worth offense and
[17:36] something that is extremely expensive when the object is not then needed at the end. Um you can use um the lazy services in Um you can use um the lazy services in Symfony with the YAML XML or PHP syntax
[17:51] Symfony with the YAML XML or PHP syntax here specifying lazy in XML it's an attribute in PHP it's a method on the service um builder or with the attribute based service configuration you can use autoconfigure
[18:06] configuration you can use autoconfigure lazy true to make a service lazy so this means in Symphfony DIC See services are not lazy by default and that's important to remember. Um however like there's a lot of optimization in
[18:20] Symfony for example for services created by the event uh or used with the event dispatcher. So listeners and subscribers there are specific mechanisms in there
[18:32] to make them lazy by default. How does this look in a symfony application? I have a small block uh example application that we can look at. So we have the block controller here, some
[18:45] services. It's doing some work. So let's try to find out which services are um configured to be lazy and we can do that by opening up the container debug XML in
[18:58] by opening up the container debug XML in Symfony which is generated. So um what we can do here now it contains all the services as an XML definition and then services as an XML definition and then we can search for lazy true and we can
[19:11] we can search for lazy true and we can see that uh it automatically added the see that uh it automatically added the lazy um uh setting to post repository domain repository. So repositories are being uh made lazy. And then we um
[19:28] being uh made lazy. And then we um will also look at the entity manager which is marked as lazy. So here we can see doctrine entity
[19:41] manager is lazy. So now we go into the generated PHP code. So, Symfony uses all the configuration to generate
[19:55] the PHP code that the uh dependency injection container is then using in production. Uh the reason for this is that it uh wants to avoid parsing the configuration files on every request. And this is a huge performance
[20:09] improvement for Symfony's dependency injection container compared to other injection container compared to other containers that don't use um uh code generation. So every service is mapped to a function
[20:26] in this um uh factory code that is generated and we are looking at the factory method for the entity manager service here. So we see it has an
[20:38] service here. So we see it has an optional lazy load true boolean and if the boolean is true then it lazy loads the service by using
[20:50] then it lazy loads the service by using reflection class opt entity manager new lazy ghost. So in this case it's using new lazy ghost not new lazy proxy as we new lazy ghost not new lazy proxy as we have seen and uh symfony has clever uh
[21:05] code um to decide whether it needs to be ghost or proxy that is something we ghost or proxy that is something we cannot dive into in detail. So we have a lazy ghost and it has access to the container and for the initializer it
[21:20] calls itself and instead of true it passes the current uh instance of the proxy as a second argument. So this is something I find a little bit weird um for understanding the code. Um I understand
[21:36] why they did it because nobody reads this code anyways. But uh um like for understanding it it's a little bit complicated that lazy load here is either a boolean that is true or it's an object which is a ghost instance of this
[21:53] service. So if we have the ghost instance we don't go into this branch but instead into this. So this is the actual factory into this. So this is the actual factory code and um it loads some files if they
[22:08] haven't been before and then it calls the constructor on the lazy load instance and the constructor of the doctrine entity manager requires the connection and some other information. Um we have seen that before in our
[22:25] uh own service code. So, entity manager requires the connection and the configuration object. And this is essentially the same code like in our example, but the code generated Symfony code that um adds just
[22:39] like all the complex and powerful functionality that Symfony provides. But in the end, it's the same. So, we have this uh lazy doctrine entity manager this uh lazy doctrine entity manager here. And um when the entity manager is
[22:55] here. And um when the entity manager is not used then it will not be constructed at runtime saving all the the autoloading and construction cost uh in your application. I hope you learned something about the
[23:08] lazy objects RC in this video and how uh dependency injection containers will dependency injection containers will benefit from its functionality um by like skipping um the unnecessary autoloading and uh object graph
[23:22] creation. This is not the only use case that you can use the lazy objects RFC for. There's another video on this channel where I explain how to use it for an RM by using doctrine as an example. And if you like to watch uh
[23:38] performance related content for PHP, um please subscribe to our newsletter which will uh give you access to uh new videos like this and also the content in our
[23:50] blog. You will find the subscription link uh down in the description. See link uh down in the description. See you.
⚡ Saved you 0h 24m reading this? Transcribe any YouTube video for free — no signup needed.