I once worked on something similar:
FMP v.14 (so this was before some threading change for portal rendering introduced in v.15)
Use case was a portal used to display the list part of an email viewing interface, and there were lots of icons and formatting in the list that needed to hide or show or otherwise change formatting, determined by email status - driven by field data local to the portal context, some related data, and a variety of global fields which allowed the user to click display settings.
I worked on a cache approach using a variable (I suspect I used a $$global, and not a $local). There were no native JSON functions at the time, so I am guessing that I probably used a return-delimited list as the data structure stored in the variable. (I haven't seen the code for years)
IIRC, the logic for accessing the cache data was to first check a separate variable which stored the primary key from the portal row for the last time that cache data was recalculated. And so, for the first layout object that needed to access the cache on a portal row, there would be the overhead to recalculate the all of the necessary cache values to render a row, and then these would be stored. All subsequent attempts to access the cache data would first involve the same conditional check against the recorded primary key -- since those subsequent checks would match the previous recorded key, the cache data would be returned without any recalculation.
While I did not experiment with comparing other means of storing the values, I did experiment with turning the cache feature on and off, and I did notice a subjective difference in the speed at which I could scroll through the portal. That is, it did seem to help at some human-perception level which I did not attempt to quantify.
I am going to take a guess at a basic outline of how my calcs worked. I am pretty sure that I made use of custom functions which referenced both variables and schema, and I know that this may not suit you, but the use of custom functions was not essential to the strategy -- the same could be achieved without them. I personally have a distaste/aversion for referencing schema within a custom function, but I made a trade-off in that regard because the custom functions helped improve the readability of my layout object conditional calculations.
So, I think the approach was something like this:
Define a custom function that would recalculate all of the constraints that are needed to render a portal row. These constraint results were free of newline chars, and so it was safe to have this custom function return the values as an ordered CR-delimited list. Lets call this function: CalculateConstraints()
Then there was the function that served as the accessor to the cache data.
It probably looked something like this:
AccessConstraints( PortalRowId ):
~id = PortalRowId
~id = $$_cache_row_id ; $$_constraint_cache ;
$$_constraint_cache = CalculateConstraints();
$$_cache_row_id = ~id
And then each conditional layout object calculation would access a constraint via:
GetValue( AccessConstraints( MyPortalContext::ID ); n ) // n specific to each constraint
I do not recall if I further wrapped the above in another layer of custom functions with hardcoded values for n for the sake of readability. That would be something that I would be apt to do, but at the same time I had to consider overhead -- I can not remember which I decided.
What I do recall is that there was some strategy involved in crafting the AccessConstraints function itself -- strategy which was specific to the needs of this solution:
If all constraints are entirely independent of one another, and all are always necessary and subject to change with each portal row, then I believe this makes for the simplest case, as AccessConstraints must determine everything, and order of determining values may not matter much due to the independence.
If, on the other hand, some constraints depend on others and/or it may not be necessary to recalculate everything for each portal row (either because not all values are subject to change, or not all are required), then the game of strategy begins, and one has to decide the optimum structure of AccessConstraints (or even if more than one such AccessConstraints function is necessary). But -- the top level game plan is the same: Calculate what must be calculated, and cache by Portal Row ID.
Again - I will call out that this was with v.14, and so many things have changed, including the option of using JSON (I suspect CR-delimited might perform faster), and the under-the-hood code for rendering a portal.
I will also say that this was one of those cases where I felt some regret about leaving this code behind for another developer to understand. It really is not difficult to understand, but it is unfamiliar, and I sometimes wondered whether anyone ever had to work on that portal and struggled to make sense of it.
All the best to you -- I hope this may be of some help or inspiration.
p.s. My uncertain recollection is that front-to-back layering order of layout objects was the primary factor in determining the order of layout object calculations, but I can't claim to have confirmed this any time recently.