Are 'shared' and '_G' really as bad as people make them seem?

Hi hi,

I’m currently writing a custom framework to build upon my skillset. I’ve stumbled into a slight inconvenience while writing though. The architecture of my framework is different compared to other frameworks and attaining modules from a module deep in a hierarchy is a huge pain. Take a look:

image

Within the ‘Hoverboard’ module, I’m trying to require the modules ‘Trove’ and ‘debug’. To do that I’ve settled for using shared. I’m well aware that there’s a race condition problem with shared, however I’ve made the ‘shared’ modules load in before everything else, so there’s no problem there.

But while reading upon shared I’ve realized that it’s really frowned upon for certain reasons. There are a few topics that have debates about shared and _G, such as this one and this one and even this one.

Some people are saying it’s fine to use depending on the circumstance, and others beg to differ. So which one is it? What’s the performance difference between modules and shared? What’s the memory difference?

It makes me think that the concept or saying “Using shared is bad (or good)” is just an opinion, and nobody is doing actual research before posting replies. It’s quite infuriating for me because I’m not well spoken in this stuff and I’m getting really mixed messages from these users.

So that’s my question; are shared and _G really as bad as they’re claimed to be?

Additionally, here’s how I store the modules within shared:

-- ...
-- functions
local function initiateShared()
	local clientShared = clientFolder:WaitForChild("shared")

	for index, object in ipairs(clientShared:GetChildren()) do
		shared[object.Name] = require(object)
	end
end

-- I'm storing all the modules within the shared table
initiateShared()

-- and then I'm initiating the client side framework
framework.initiate()

Thanks in advance! :slight_smile:

Alright, how about we settle this down then.

So far I’ve seen more people opposing the use of _G and shared. However, all arguments against don’t apply to all uses cases.

Except in some niche situations, module scripts have more strengths and advantages over global tables than global tables have over module scripts.

Personally, I rather avoid global tables altogether.


Main issues with global tables

  • race conditions → a script might be attempting to access a non-existing value (you’ve taken care of this)

  • worse intellisense and autocomplete

  • mutable global variables are a very bad practice → what if two or more scripts try to modify this variable?

  • organizational dillema → lots of elements in a global table can appear cluttered

  • chance to accidentally overwrite a variable with a another, unrelated one, with the same name/stored to the same key

  • global tables can do nothing that module scripts cannot do

  • local access is undisputably faster than global (follow up below)

  • _G can look ugly

  • they tend to promote bad practices → dangerous trap


Some truths:

  • global access is not as slow as it is occasionally claimed → performance difference is not as pronounced as in native Lua or most other languages because of Luau optimizations; in fact, it may be considered negligible in a lot of cases.
    Not a definitive info, but _G access vs local access is (only) ~ 2.4 times slower.

  • global tables can sometimes provide easier access to immutable utilities and some read-only variables

I might revisit this post later to see if I missed something.

Update. Applied some micro revisions.

6 Likes

It’s not just worse, it literally doesn’t exist lol. _G is an inherently unpredictable method of data storage/transaction so there’s absolutely no way for the intellisense to predict anything.

You can still add intellisense by manually overriding the _G variable with a type-annotated local version like this, but it’s super tedious and just not worth it. For any future readers, just stick to modulescripts my man

image

3 Likes

Apologies for the late reply, I’ve been busy.

  • Race conditions

Thanks for the reply! I see the differences, however how do these actually pair when set in an actual well set framework environment? In my case, race conditions can’t happen because the thread initiates the ‘shared modules’ before any other module loads in. Of course, this can be a huge deal for other people, but again, for my framework, race conditions won’t occur.

  • Worse autocomplete

This is of course true, it won’t ever autocomplete anything because it’s outside the what I like to call ‘scope’ of the script. And as @Prototrode stated, it won’t autocomplete ever. However (yes there’s a however) in my use case, I’m only writing short declarations such as shared.Trove or shared.cache. Even if it had perfect intellisense, for me personally, it’d still be faster to just write out the word.

  • Mutable global variables

In my use case, I can’t run into this problem. As I’ve stated above, I only use shared to store modules. I don’t redeclare anything, ever.

  • Organization

Well, I’m not really printing the table. And if it were to be printed, it’d just look like this:

{
	["Trove"] = tablex000000, -- memory address
	["debug"] = tablex000001,
	["getModules"] = tablex000002,
}

If you’re referring to something else, that’s my bad, but I don’t particularly think the organization matters in something which is never displayed.

  • Accidental overwrites

As I’ve stated above, I only ever use shared to store modules, and if I did make the mistake of doing something like:

shared.Trove = 101011

I think the console would emit the cause immediately because it’d try to index a function/method but instead receives x.

  • Global table(s) vs module scripts

I understand that module scripts are extremely more powerful than global tables; there’s no arguing that. But in my use case, the difference is so fractional, it’s like comparing Vector3.zero over Vector3.new(0, 0, 0). Of course I’m exaggerating, they don’t have a similar speed difference to a built in class. Any who, my point is that for my use case, the difference in what each component can and cannot do isn’t a dealbreaker for me.

If I’ve misinterpreted what you meant, please elaborate!

  • Looks

Well clean code and efficient code depends on what you’re doing. I believe if you’re using a complex algorithm or formula which is being computed at 1/60Hz, I think you should prioritize efficiency over looks. However if you’re creating a clickable button that does something arbitrary, sure, all for clean and good looking code.

I know that _G and or shared may not project the ideal look, but it gets the job done of communicating via other scripts.

  • Bad practices

I’m quite confused. What exactly are these bad practices? I’m not exactly sure, again, I haven’t dabbled in this stuff.

  • Conclusion

This is something that slides into my use case. I’d say the modules I use are semi-lightweight and don’t effuse much memory. Additionally, they’re utility modules. They’re not something that contains large game content, such as datastores.


I agree with @Prototrode about the intellisense, but I beg to differ on the last statement about sticking to just module scripts. I believe global variables can be helpful if used correctly; keyword, ‘correctly’. I think using global variables for database storage, part storage, service storage, etc… is a terrible idea. But what about lightweight modules?

Any who, I’m still perplexed on whether or not I should use these darn global variables or not. I need some sturdy evidence that shows the gap between module scripts and global variables. Again, it’s infuriating because I can’t conduct tests like these. I have not a shred of idea of where to begin.

Btw I forgot to mention, I don’t repeatably use shared. I call it roughly 5-10 times (depending on how many modules I’m working with) and that’s it; never any more. I reference the modules as a tuple, like so:

local troveModule, debugStick = shared.getModules.getModules("Trove", "debug")

Regardless, I appreciate both of your replies. Thank you :slight_smile:

1 Like

Your thinking sounds reasonable to me. Since your post is a bit longer, let’s start towards the end :slight_smile:

The purpose of my previous reply was to answer the leading question: Are shared and _G really as bad as people make them seem, hence it includes some of the reasons why a big portion of the dev community avoids them and advices against them. It is not intended to undermine your rationale.

Quite the opposite, the post can work in your favour. The majority of the points weren’t relevant to your particular situation, because, at least in my opinion, it falls among the mentioned niche categories, as you have reasserted by addressing each point. And those are the points that bring bad reputation upon global tables.

That would especially apply to beginners. In the past we’ve seen numerous examples of overused _G, mostly back from the non-Roblox-FE times. They have the potential to steer towards practices that bring up all the other listed negative points.

Fair enough if you ask me. While I don’t use global tables a lot, I recognize a solid use case here. Some would argue global tables should have nothing to do with production code, others that it can improve the code, namely in large-scale code structures when used to store utilities and constants or selectively accessed mutables.

At the end of the day, a perfect project is almost never a finished one, and it’s more important to choose a good practice that works for you and/or your team, as well as stay consistent.

Hopefully this elaboration serves you well!

Regarding other languages

Just like the old wait, spawn, and delay in Roblox, there’s a good deal of talk on the web about how global state is generally “evil”. Some of these arguments apply to Roblox engine and some don’t. In other languages and implementations, the circumstances are not to be overlooked, such as interpreter or compiler optimizations.

1 Like

What you have here is a programming style. In this case, a clear and well-defined way of using the language resources. the shared and _G globals. When you have a style, you have nothing to worry about.

In reality, from a programmer’s point of view, shared and _G are just modules created by default (a module returning a table), which are required by default. Nothing from the other side. When the code is compiled there is practically no difference between using roblox globals or their equivalent in modules.

Bad practice: approaches, techniques or ways of writing code that may be inefficient, error-prone, difficult to maintain or understand.

Programming styles: refer to the conventions, rules and guidelines used to write code in a consistent and readable manner. Programming styles include everything from the choice of variable names or code indentation, to the design of the code architecture.

If you think about it, a bad practice is the absence of a programming style.

It is clear, if you have a style, there is no problem, you can use globals.

1 Like

You seem like you know what you’re doing, and there really is no problem with using shared or _G for your use case. If you want to optimize your fingers, go right ahead.

There’s no gap. Yes, indexing them is slower than indexing a local module, but table indexing is an atomic operation (1e-7 to 1e-6 seconds). What is an up-to 2x slower index speed going to do? Nothing.

1 Like

Apologies for the late reply, I was sleeping.

I suppose I should’ve formed my question to be more relevant toward my use case, but I think my curiosity in terms of the opposing views on global variables was biasing my original question. My bad haha.


But if I’ve dissected the attributes that make global variables bad, with a small amount of conclusive text, how come the community still deems the global variables as something that would end the entire world?

An example would be a post made within this topic (which was a topic referenced in my conslusive post above):
(I used my quote template because I don’t know how to quote the other post)

But then another post within the same thread has an opposing view and all of a sudden, the dynamic changes:


  • Off-topic

This is slightly off-topic, but I think it does need a mention because my clarity needs it to be said. I don’t have a direct problem with who’s right and wrong. Keywords: ‘right and wrong’. Why are there opposing views within the same topic, for the exact same question? This person says it’s bad, another says it’s great, nobody corrects one another, who do I follow?

Because of how people spoke ill about global variables, others embraced that and now there’s this bias when the question “is _G bad?” arises. Nobody has any viable proof of the performance differences. And I mean that in all aspects, not just read time (indexing) but how global variables might use Luaheap (which is something that was referenced in a post I don’t recall anymore) and that strangles the memory apparently? I’d need somebody to explain that.

I just wish people did some research before posting. It just feels like a massive social media problem where one person says x thing, and everybody follows because they favour the other person for whatever reason. And as I’ve stated in my posts above, I have literally no clue how to conduct these experiments. I don’t know anything about it. That means I heavily rely on the developer forum for sufficient information from people who are greatly more versed in these situations.

Of course I don’t mean any hate toward anyone who’s posted here. But when somebody says x method is faster, I just like knowing how it’s faster, and the differences between the methods.


  • Bad practice

I understand that beginners aren’t well informed with global variables, and because of that, they may use it poorly. But that’s with everything. Beginners use remote events, remote functions and even module scripts, poorly. So because of that, should the things I listed be frowned upon for everybody? Of course not! But from my perspective, this is how people are interpreting it.

Any who, the way my framework operates is like the lucid chart below.

I personally think this is a neat and well structured framework (of course there’s always room for improvement) But is this considered bad practice? Are there better alternatives which boost performance?


Additionally to my framework, as I’ve stated:

I don’t use shared repeatably. I use it sparingly. I absoutely wouldn’t dare to use it in a 1/60Hz signal, such as .PostSimulation:

-- services
local runService = game:GetService("RunService")

-- functions
runService.PostSimulation:Connect(
	function (deltaTime: number)
		shared.counter += 1
	end
)

I understand how that code is unreasonable. But again, I use shared sparingly. To give a little more insight to how regularly I reference it, you can take a look at my framework:

image

  • Any red dots: Indicate that only a single call is made to shared, and never more afterwards.
  • Any pink dots: Indicate that a call is only made if the module’s function gets called. The module’s function only gets called from the red dots, so they’re pretty much red dots.
  • Any green dots: Indicate that something is getting declared in shared. After that, shared is never used again.

And as @Content_Corrupted019 said;

It’s pretty much the same as doing:

local sharedFolder = script.Parent.Parent.Parent.Parent:WaitForChild("shared")

-- modules
local troveModule = require(sharedFolder:WaitForChild("Trove"))
local debugStick = require(sharedFolder:WaitForChild("debug"))

I think in terms of performance, my use case; there’s almost no difference in the method I use to achieve the modules. And honestly, if I had a ton of modules to load into an important module, I’d much rather it look like this:

-- modules
local troveModule, debugStick, carController, dialogue, notifications, console = shared.getModules("Trove", "Debug", "CarController", "Dialogue", "Notifications", "Console")

Or this if it’s unable to fit into the entire line. Though it makes two calls to shared instead of one:

-- modules
local troveModule, debugStick, carController = shared.getModules("Trove", "Debug", "CarController")
local dialogue, notifications, console = shared.getModules("Dialogue", "Notifications", "Console")

Instead of it looking like this long mess:

-- variables
local sharedFolder = script.Parent.Parent.Parent.Parent:WaitForChild("shared")

-- modules
local troveModule = require(sharedFolder:WaitForChild("Trove"))
local debugStick = require(sharedFolder:WaitForChild("Debug"))
local carController = require(sharedFolder:WaitForChild("CarController"))
local dialogue = require(sharedFolder:WaitForChild("Dialogue"))
local notifications = require(sharedFolder:WaitForChild("Notifications"))
local console = require(sharedFolder:WaitForChild("Console"))

  • Conclusion

I agree here, I think as long as there’s a profound style; and or a sense of clarity, using shared is just like another tool in your arsenal.

There’s some room to elaborate, but for the most part, I agree. Though I feel like there are some arbitrary situations like how some people predefine the built in luau classes:

-- predefines
local vector3 = Vector3.new
local abs = math.abs

-- functions
local function absvector(vector: Vector3)
	return vector3(
		abs(vector.X),
		abs(vector.Y),
		abs(vector.Z)
	)
end

But I think this falls into the category of a programming style anyway.


I don’t want to keep quoting all of what you’re saying; in short, I agree with your stance on global variables and I believe they’re useful if used correctly.


I semi-know what I’m doing, but there’s no clarity on the developer forum about what’s best practice for certain use cases. And in my case, I believe I’m using shared correctly, but so many people are saying it’s bad, and so many others beg to differ.


Any who, I’ll make a poll for anybody in the future that views this topic. So any similar people in my position might gain at least a slight insight into whether their use case fits in.

  • shared and _G should never be used.
  • shared and _G can be used in a well-maintained manner.

0 voters


Thank you all for your replies. sorry if this reply is a little long, I just want to come to a successful conclusion.

Thanks again guys :slight_smile: