I'm making this high-level guide to help save other newbies some headache for procedurally generating maps with huge amounts of tiny tiles (1,000,000+).

Essentially I am working on a 2D dungeon platformer with procedurally generated, high-fidelity dungeon rooms. There are plenty of great tutorials for this already so I will save my breath on this. And while TileMaps work great, huge performance issues arise while generating maps with huge amounts of tiles - the initial generation step takes forever but also low-FPS occurs when playing the map. So you might be wondering, why not just load/unload the TileMap in chunks as you go? The issue with this approach is that I'm using tiny tiles (4x4 px) in large amounts to give the map a high-fidelity look; and even in relatively small chunks, I was still having performance problems.

My solution: use a combination of TileMap + MultiMesh Instances.
The thought process is that most tile objects do not need to have individual collision/navigation layers, so we can greatly reduce the amount of GPU draw() calls by having these display tiles share resources and be rendered as a single-object. MultiMeshes allow us to render large amounts of meshes (individual tiles in this case) very efficiently.

So, in implementation, I still use TileMaps for the floor tiles with collision layers, but use tile-specific MultiMeshes for everything else. My code looks like this:

Init the MultiMesh-tile map:

@onready var multimesh_tiles = {
	$WaterMultiMesh: [],
	$GrassMultiMesh: [],
	$JungleMultiMesh: [],
}

Where I translate noise to tiles:

#Other Biomes
else:
        #plains
	if moist < 0.9 and temp < 0.6:
		~~set_cell_tile(pos, random_tile(biome_data["plains"]))~~
		self.multimesh_tiles[$GrassMultiMesh].append(pos)
	#jungle
	elif moist >= 0.4 and temp > 0.6:
		~~set_cell_tile(pos, random_tile(biome_data["jungle"]))~~
		self.multimesh_tiles[$JungleMultiMesh].append(pos)

Where I set the transforms of each individual MultiMesh instance:

func set_multimesh_instance_transforms():
       for mm in self.multimesh_tiles:
		mm.multimesh.instance_count = len(self.multimesh_tiles[mm])
		for i in mm.multimesh.instance_count:
			mm.multimesh.set_instance_transform_2d(
				i, Transform2D(0.0, map_to_local(self.multimesh_tiles[mm][i]))
			)

https://docs.godotengine.org/en/stable/tutorials/performance/using_multimesh.html#multimesh-example
https://docs.godotengine.org/en/stable/classes/class_multimesh.html#class-multimesh-method-set-instance-transform-2d

    This method allows for efficient map generation machinery while also maintaining consistent performance in playing the map. I hope this guide can help point future developers in the right direction and save them some of the frustrations of getting a high-fidelity procedurally generated maps off the ground.

    4 months later

    jahveen " This seems really useful, thanks for posting this. I have been struggling with tilemap performance myself, specifically with procedural generation of large or infinite worlds. Would there be any chance you could create an example project using this method? I am using a custom autotiling method, a 4-bit neighbor check (north, south, east, and west), and this gets run when generating the tiles, and destroying/creating terrain by the player. It would help to see more of your scripts in this example, so I can figure out how to implement the autotiling and worldgen system to accommodate this approach. Thanks again!