Help optimising non-humanoid NPCs!

My game contains a LOT of NPCs spread across a giant map. Previously, I simply used Humanoid:MoveTo() to control their movement, and that worked fine for fewer humanoids, eventually it became too much for the server to handle.

I decided to redo my NPC system so that it follows this pattern:

  1. Server performs a raycast down from a random position offsetting the NPC’s origin.

  2. Client detects this information and incrementally moves the NPC toward the given position, using CFrames in a RenderStepped loop.

  3. If the NPC is too far above the ground, a ray is created down and the NPC is re-positioned to the floor.

This seems to have worked fine, however I’m worried that raycasting every frame for 300+ objects may be a bit too taxing on the client. Here is my code currently:

function critter:Step(dt)
	local movePos = self.model:GetAttribute("MovePos")
	if not movePos or self.connections["hide"] then return end

	local pivot = self.model:GetPivot()

	if ((pivot.Position - movePos)*(Vector3.xAxis+Vector3.zAxis)).Magnitude < self.maxSize then 
		self:Animate("Walk",false)
		return 
	end

	local lookAt = Vector3.new(movePos.X,pivot.Position.Y,movePos.Z)
	local translateFactor = pivot.LookVector * self.data.speed * dt

	local downRay = workspace:Raycast(pivot.Position+Vector3.new(0,50,0),Vector3.new(0,-100,0),params)
	if not downRay then return end

	self.model:PivotTo(pivot:Lerp(CFrame.new(downRay.Position+Vector3.new(0,self.size.Y/2,0),lookAt),.1)+translateFactor)
	self:Animate("Walk",true)
end

Any help coming up with a better or more performant version of this system is greatly appreciated.

Kind of late to respond but, you could make it so that they will only do it if they are within view or within a certain distance in order to actually render it on the client