How does lazy loading modules on ROBLOX work?

So I’m currently changing the way modular services and classes are loaded internally in my Framework/“Engine”.

I was reading the wiki on “lazy loading”, as I’ve seen developers “lazy load” modules on Roblox before. I read some code by @Crazyman32 that lazy loads modules:

function LazyLoadSetup(tbl, folder, exposeAero)
	setmetatable(tbl, {
		__index = function(t, i)
			local obj = require(folder[i])
			if (type(obj) == "table") then
				if (exposeAero) then
					setmetatable(obj, {__index = AeroServer})
				end
				if (type(obj.Init) == "function") then
					obj:Init(AeroServer)
				end
			end
			rawset(t, i, obj)
			return obj
		end;
	})
end

While I understand what some of this is doing (:Init() just initializes a service module while exposing the engine to it, etc.), I’m still having a bit of trouble understanding how lazy loading works on Roblox.

Would a kind soul explain to me how lazy loading a module works, and why a metatable is required to do it?

Thanks. :slight_smile:

7 Likes

The concept of “lazy loading” is that a resource doesn’t actually get loaded into memory until it is first used. It can also help your start-up performance. You will see this used a lot in mobile applications, or other performance-limited environments.

In Roblox, modules already act in this manner, as they do not run any code until required. So, you simply need to avoid loading your modules until you need them if you’re looking to follow this principle.

The code you posted from my framework is a bit different, since it allows you to access modules in dot-notation from a table, but it also sets up a lot of other jargon in relation to my framework itself. All of that other stuff isn’t important toward the concept.

If you’re looking to have a similar setup (where modules are only required once they are first needed), then you could set it up like this:

local moduleFolder = game:GetService("ReplicatedServices").MyModules

local modules = {}
setmetatable(modules, {
	__index = function(t, moduleName)
		local module = require(moduleFolder[moduleName])
		modules[moduleName] = module
		return module
	end;
})

-- Load a module:
local someModule = modules.SomeModule

In that example, when you try to access a module in modules that doesn’t exist, it requires the module and puts it into the table. The key to remember is that the __index metamethod only fires if the queried index doesn't exist, thus we know to load the module and put it into the table. The next time the same item is accessed in the modules table, it will simply pull the right item and avoid firing the __index metamethod.

25 Likes

Ahh, this makes much more sense. And I see why the metatable is required, this is to prevent actually loading the module until you call apon the module.

Thanks!

2 Likes

Does this work with the age old module-calling-each-other paradox?

Modules should not be requiring each other. Doing so is a sign of bad design. They can use each other, but they shouldn’t load each other. In other words, after both are required, they can call each other just fine. Some CS people will still scream that this results in tight two-way coupling (I guess “circular dependency” might be a more appropriate term), but again it comes down to how well you’ve designed your system.

5 Likes

when you say, “they can call each other just fine” can you show me the difference? I think I’m just slightly confused.

By “use eachother”, he means that they can access eachother through a 3rd party module loader. This is different from them requiring eachother.

Here’s an example.

Modules calling eachother via a 3rd party module loader
Module loader server script

local Modules={}
local ModulesFolder=SomeFolderThatHoldsModulescripts

--[[ Load modules ]]--
for _,Modulescript in pairs(ModulesFolder:GetChildren()) do
    local Module=require(Modulescript)
    setmetatable(Module,{__index=Modules})

    Modules[Modulescript.Name]=Module
end

--[[ Do the greetings ]]--
Modules.Module1:GreetAndLeave()

Modulescript 1

local Module1={}

function Module1:GreetAndLeave()
    self:SayHello()
    self.Modulescript2:SayGoodbye()
end

function Module1:SayHello()
    print("Hello!")
    self:Wave()
end

function Module1:Wave()
    print("*Waves*")
end

return Module1

Modulescript 2

local Module2={}

function Module2:SayGoodbye()
    print("Goodbye!")
    self.Module1:Wave()
end

return Module2

Modules directly requiring eachother (you shouldn’t do this)
Modulescript 1

local Module2=require(path.to.Modulescript2)

local Module1={}

function Module1:GreetAndLeave()
    Module1:SayHello()
    Module2:SayGoodbye()
end

function Module1:SayHello()
    print("Hello!")
    Module1:Wave()
end

function Module1:Wave()
    print("*Waves*")
end

return Module1

Modulescript 2

local Module1=require(path.to.Modulescript1)

local Module2={}

function Module2:SayGoodbye()
    print("Goodbye!")
    Module1:Wave()
end

return Module2
3 Likes

I know this is a year-old thread but it got bumped. I’ve never heard of “lazy loading” a module script before. After reading your reply, the concept makes sense in real applications, but on Roblox are there really performance benefits? Seems like if actually loading a module was causing a significant performance drop that would be because the programming in the module is poor, not because it needs to be loaded.

It’s more about memory. Why load a module into memory if it’s not being used? I use it so I can have modules ready to go but don’t necessarily have to use them. It’s a simple solution when you don’t have any package management.

1 Like

Maybe it’s because I don’t micro-optimize then. Lazy loading still seems kind of pointless on Roblox because you can’t “unload” a module. Once it’s loaded, it’s loaded, so all that memory is going to get used anyway. And on that note, how much impact is a module going to take on memory in the first place?

4 Likes

Ah I see! By connecting a metatable to the module, you’re allowing the module to use the __index metamethod as a sort of “redirect” back to the module directory, if you will. Thanks for clearing this up.

The only reason I’m looking at this sort of stuff is because I was trying to find away to get around the module paradox as I have found myself entering this way too often and wanted to see if there was a way around it. There seem to be quite a number of different methods to do this, each varying ever so slightly from each other.

Some using inits and passing the module through in the same central script, some using inits in one central script and requiring the module from each module script and then these ones which use metatables and loops to allow module communication through metamethods.

2 Likes

Here’s some already existing frameworks that solve these issues:

And here’s my own wip framework, inspired from “AeroGameFramework”:

1 Like