Here are some changes that made it twice faster on my machine:
1. Use File.stream! instead of File.open! + IO.read(..., :line). Typically, Elixir/Erlang favors distribution which means File.open/2 is going to open up a process (lightweight thread of execution) so you can send it across nodes. The downside is that every operation has the overhead of going through this process. This is not an issue in many cases but in tight loops like the example above they are. Using File.stream! solves this issue because the "file descriptor" is never exposed, so we can optimize and avoid creating processes besides using other options like :read_ahead which are useful on read only modes (plus the code is cleaner).
2. Use Dict.update/4 instead of traversing the Dict twice
There are other things to consider:
1. In contrast to Ruby, all operations in the String module are Unicode based. So when you call String.strip/1, it looks up all whitespace characters in the unicode codebase. This is usually the safest way to do such string operations but it may add a bit of overhead when comparing to languages that do not perform this operation
2. Do perform a protocol consolidation. In order to allow code reloading in development, protocol dispatch may introduce some overhead. You can compile protocols by compiling or running your escript with MIX_ENV=prod:
$ MIX_ENV=prod mix escript.build
Or:
$ MIX_ENV=prod mix run -e "Anagrams.main(System.argv)" -- path/to/file
After performing protocol consolidation and my fixes above, it took 3s on my old Macbook Pro from the original 11s when processing /usr/share/dict/words.