In a case like this, you are going to want to Pool your world tiles. The idea is to pre-load a fixed amount of tiles that you will be re-using during the rendering of your world.
When setting a tile into the world, you would typically set certain properties such as its position and texture. The rest of the object is already ready to go in memory.
The idea is to try to retain some tiles on the screen and only return them into your Pool when they are far enough. This way, if you revisit an area that was already set, you probably do not have to process as many tiles.

Some sample code that was used to achieve the effect in the gif above:
List<Tile> LoadedTiles = new List<Tile>();
public void Refresh()
{
// Determine your start/end X and Y coordinates (viewable area)
// Make sure to add some padding to ensure you don't see any flickering on the sides
int startx = View.x < 0 ? (int)(View.x + (-View.x % 1)) : (int)(View.x - (View.x % 1));
int starty = View.y < 0 ? (int)(View.y + (-View.y % 1)) : (int)(View.y - (View.y % 1));
startx -= 1;
starty += 1;
int endx = startx + (int)(View.width) + 2;
int endy = starty - (int)(View.height) - 2;
// Keep track of visible tiles in this refresh
HashSet<Tile> inCurrentView = new HashSet<Tile>();
// Loop through all the tile indices inside of our current view port
for (int i = startx; i <= endx; i += 1)
{
for (int j = starty; j >= endy; j -= 1)
{
int x = i;
int y = j;
// Make sure your index values are not negative.
// In this case we are wrapping the world around on the X-axis. If you are not wrapping, you need
// to figure out coordinate system that will not result in negative values.
x = MathHelper.Mod (x, Generator.Width);
y = MathHelper.Mod (y, Generator.Height);
// Fetch the tile data at this index - Store in our hashset
Tile tile = Generator.GetTile (x, y);
inCurrentView.Add(tile);
// If the tile being processed is not loaded, then get a Tile from our Pool and load it into view.
if (!LoadedTiles.Contains(tile))
{
if (tile.PoolableGameObject.Item == null)
{
if (TilePool.AvailableCount > 0)
{
// Get a tile GameObject from our Tile Pool
Pool<PoolableGameObject>.Node node = TilePool.Get();
node.Item.Set(tile, new Vector2(i, j));
LoadedTiles.Add(tile);
}
}
}
else {
// tile already loaded, refresh it
tile.PoolableGameObject.Item.Set(tile, new Vector2(i, j));
}
}
}
// Get list of Tiles that are currently outside our view
// This will remove every single tile that is not in view.
// You can refine this to only return tiles when a lower threshold of tiles remaining is met.
List<Tile> toRemove = new List<Tile>();
for (int i = 0; i < LoadedTiles.Count; i++) {
Tile tile = LoadedTiles[i];
if (!inCurrentView.Contains(tile))
toRemove.Add(tile);
}
// Return tiles that are no longer in view back into our Pool to be re-used
for (int i = 0; i < toRemove.Count; i++) {
Tile tile = toRemove[i];
LoadedTiles.Remove(tile);
TilePool.Return(tile.PoolableGameObject);
}
}