After seeing what you have achieved with streaming, I decided to take your advice about removing the code dealing with unloading old chunks and there were immediate improvements to performance. I also removed all conditions that were not necessary in generating a new block and I have narrowed it down to this set of code:
local BlockDensity = GetBlockDensity(x,y,z)
if BlockDensity > 0 and BlockDensity < .2 then
local IsCaveWall = false
local IsSurfaceBlock = false
-- Find density values of adjacent blocks
if GetBlockDensity(x,y+1,z) <= 0 then IsCaveWall = true; IsSurfaceBlock = true end
if not IsCaveWall then
if GetBlockDensity(x,y-1,z) <= 0 and not IsCaveWall then IsCaveWall = true end
if GetBlockDensity(x+1,y,z) <= 0 and not IsCaveWall then IsCaveWall = true end
if GetBlockDensity(x-1,y,z) <= 0 and not IsCaveWall then IsCaveWall = true end
if GetBlockDensity(x,y,z+1) <= 0 and not IsCaveWall then IsCaveWall = true end
if GetBlockDensity(x,y,z-1) <= 0 and not IsCaveWall then IsCaveWall = true end
end
if IsCaveWall then
local BlockCF = CFrame.new(Vector3.new(3*x, 3*y, 3*z)) + Vector3.new(3,3,3)/2
if IsSurfaceBlock == true then CreateBlock(game.ReplicatedStorage.PlacementModels.GrassBlock, BlockCF)
else CreateBlock(game.ReplicatedStorage.PlacementModels.DirtBlock, BlockCF) end
else
return
end
else
return
end
Now, if possible, I would like to know if there is a better way to determine which blocks are exposed to the surface without relying on the density values of adjacent blocks because at this point the only condition for a block to spawn (that is impacting performance) is its own density must be above 0 and it must be exposed to the surface.
Another possible way I found that I could optimize things - skip iterating over any chunk that will not have any blocks generated inside it. But, of course, there is a problem with this - how would one predict whether or not a chunk will contain visible blocks without using iteration? I have been trying to work out a solution to this for the past couple of days and I have come up with something fairly efficient, but it does occasionally yield inaccurate results which produces weird holes in the terrain caused by these faulty predictions. Here is an image to demonstrate:

Here is the code handling these predictions:
local function RenderChunkPlane(RenderY)
for RenderX = -RenderDistance, RenderDistance do
for RenderZ = -RenderDistance, RenderDistance do
local ChunkPos = Vector3.new(ChunkMapPos.X+RenderX, ChunkMapPos.Y+RenderY, ChunkMapPos.Z+RenderZ)
local ChunkPosWorld = ChunkPos*3*ChunkWidth
local ChunkDistance = (RenderPos - ChunkPosWorld).magnitude
local CharMoveDistance = (LastRenderPos - RenderPos).magnitude
local ChunkXExist, ChunkYExist, ChunkZExist = ChunkExists(ChunkPos)
if CharMoveDistance <= MaxWalkDistance then
local _, BottomPos = GetChunkMapPos(ChunkPosWorld - Vector3.new(0,ChunkWidth,0)*1.5)
local _, TopPos = GetChunkMapPos(ChunkPosWorld + Vector3.new(0,ChunkWidth,0)*1.5)
local BottomDensity = GetBlockDensity(BottomPos.X, BottomPos.Y, BottomPos.Z)
local BottomDensityCorner1 = GetBlockDensity(BottomPos.X+NearestInt(ChunkWidth/2), BottomPos.Y, BottomPos.Z+NearestInt(ChunkWidth/2))
local BottomDensityCorner2 = GetBlockDensity(BottomPos.X-NearestInt(ChunkWidth/2), BottomPos.Y, BottomPos.Z+NearestInt(ChunkWidth/2))
local BottomDensityCorner3 = GetBlockDensity(BottomPos.X+NearestInt(ChunkWidth/2), BottomPos.Y, BottomPos.Z-NearestInt(ChunkWidth/2))
local BottomDensityCorner4 = GetBlockDensity(BottomPos.X-NearestInt(ChunkWidth/2), BottomPos.Y, BottomPos.Z-NearestInt(ChunkWidth/2))
local TopDensity = GetBlockDensity(TopPos.X, TopPos.Y, TopPos.Z)
local TopDensityCorner1 = GetBlockDensity(TopPos.X+NearestInt(ChunkWidth/2), TopPos.Y, TopPos.Z+NearestInt(ChunkWidth/2))
local TopDensityCorner2 = GetBlockDensity(TopPos.X-NearestInt(ChunkWidth/2), TopPos.Y, TopPos.Z+NearestInt(ChunkWidth/2))
local TopDensityCorner3 = GetBlockDensity(TopPos.X+NearestInt(ChunkWidth/2), TopPos.Y, TopPos.Z-NearestInt(ChunkWidth/2))
local TopDensityCorner4 = GetBlockDensity(TopPos.X-NearestInt(ChunkWidth/2), TopPos.Y, TopPos.Z-NearestInt(ChunkWidth/2))
local ChunkIsEmpty = false
if BottomDensity <= 0 and BottomDensityCorner1 <= 0 and BottomDensityCorner2 <= 0 and BottomDensityCorner3 <= 0 and BottomDensityCorner4 <= 0 and TopDensity <= 0 and TopDensityCorner1 <= 0 and TopDensityCorner2 <= 0 and TopDensityCorner3 <= 0 and TopDensityCorner4 <= 0 then ChunkIsEmpty = true end
if BottomDensity > 0 and BottomDensityCorner1 > 0 and BottomDensityCorner2 > 0 and BottomDensityCorner3 > 0 and BottomDensityCorner4 > 0 and TopDensity > 0 and TopDensityCorner1 > 0 and TopDensityCorner2 > 0 and TopDensityCorner3 > 0 and TopDensityCorner4 > 0 then ChunkIsEmpty = true end
if not ChunkIsEmpty then
RenderChunk(ChunkPos)
end
else
CharacterMoved = true
break
end
end
if CharacterMoved then break end
end
end
RenderChunkPlane(-1)
RenderChunkPlane(0)
for RenderY = -RenderDistance*2, RenderDistance*2 do
if RenderY ~= 0 and RenderY ~= -1 then RenderChunkPlane(RenderY) end
if CharacterMoved then break end
end
Basically, what the code is doing here is getting the density values of all 8 corners of the chunk and if all these values are either collectively above 0 (underground) or collectively below 0 (in the air) then the chunk is not going to contain blocks. Obviously, its not completely accurate and it all feels kind of hacky and inefficient so I figured that there is some kind of more simplistic method that I’m missing here?
Finally, after everything I have done so far to optimize my code based on your advice, there is a new problem - as you probably know already, using default smooth terrain is several times more efficient in terms of memory than using customized blocks made out of part instances. Because of this, after switching my game over to streaming enabled there was a strange lag building up as my character walked around and loaded more chunks, even with streaming set at minimum range. I would assume that this lag is being caused by the buildup of parts on the server as more chunks are loaded (because they are no longer being unloaded)? If you would like to take a look for yourself, I updated the uncopylocked place I shared with the new version of the code and streaming enabled.